@@ -499,34 +499,15 @@ def multi_line_conditions(self, info):
499499 raise RefactoringError (
500500 "Extracted piece should contain complete statements."
501501 )
502-
503- if self ._is_region_incomplete_block (info ):
502+ unbalanced_region_finder = _UnbalancedRegionFinder (
503+ info .region_lines [0 ], info .region_lines [1 ]
504+ )
505+ unbalanced_region_finder .visit (info .pymodule .ast_node )
506+ if unbalanced_region_finder .error :
504507 raise RefactoringError (
505- "Extracted piece cannot contain the start of a block without the end"
508+ "Extracted piece cannot contain the start of a block without the end. "
506509 )
507510
508- def _is_region_incomplete_block (self , info ):
509- """
510- Is end more indented than start, and does that level continue outside the region?
511- If so, this is an incomplete block that cannot be extracted.
512- """
513-
514- def get_effective_indent (lines , line ):
515- if found_line := sourceutils .find_nonblank_line (lines , line ):
516- return sourceutils .get_indents (info .pymodule .lines , found_line )
517- return None
518-
519- start_line = info .region_lines [0 ]
520- end_line = info .region_lines [1 ]
521- start_indent = get_effective_indent (info .pymodule .lines , start_line )
522- end_indent = get_effective_indent (info .pymodule .lines , end_line )
523- end_next_indent = get_effective_indent (info .pymodule .lines , end_line + 1 )
524- return (
525- end_next_indent is not None
526- and start_indent < end_indent
527- and end_next_indent >= end_indent
528- )
529-
530511 def _is_region_on_a_word (self , info ):
531512 if (
532513 info .region [0 ] > 0
@@ -1122,6 +1103,34 @@ def _ClassDef(self, node):
11221103 pass
11231104
11241105
1106+ class _UnbalancedRegionFinder (_BaseErrorFinder ):
1107+ """
1108+ Flag an error if we are including the start of a block without the end.
1109+ We detect this by ensuring there is no AST node that starts inside the
1110+ selected range but ends outside of it.
1111+ """
1112+
1113+ def __init__ (self , line_start : int , line_end : int ):
1114+ self .error = False
1115+ self .line_start = line_start
1116+ self .line_end = line_end
1117+
1118+ def generic_visit (self , node : ast .AST ):
1119+ if not hasattr (node , "end_lineno" ):
1120+ super ().generic_visit (node ) # Visit children
1121+ return
1122+ ends_before_range_starts = node .end_lineno < self .line_start
1123+ starts_after_range_ends = node .lineno > self .line_end
1124+ if ends_before_range_starts or starts_after_range_ends :
1125+ return # Don't visit children
1126+ starts_on_or_after_range_start = node .lineno >= self .line_start
1127+ ends_after_range_ends = node .end_lineno > self .line_end
1128+ if starts_on_or_after_range_start and ends_after_range_ends :
1129+ self .error = True
1130+ return # Don't visit children
1131+ super ().generic_visit (node ) # Visit children
1132+
1133+
11251134class _GlobalFinder (ast .RopeNodeVisitor ):
11261135 def __init__ (self ):
11271136 self .globals_ = OrderedSet ()
0 commit comments