| 179 | |
| 180 | |
| 181 | def getstatementrange_ast( |
| 182 | lineno: int, |
| 183 | source: Source, |
| 184 | assertion: bool = False, |
| 185 | astnode: ast.AST | None = None, |
| 186 | ) -> tuple[ast.AST, int, int]: |
| 187 | if astnode is None: |
| 188 | content = str(source) |
| 189 | # See #4260: |
| 190 | # Don't produce duplicate warnings when compiling source to find AST. |
| 191 | with warnings.catch_warnings(): |
| 192 | warnings.simplefilter("ignore") |
| 193 | astnode = ast.parse(content, "source", "exec") |
| 194 | |
| 195 | start, end = get_statement_startend2(lineno, astnode) |
| 196 | # We need to correct the end: |
| 197 | # - ast-parsing strips comments |
| 198 | # - there might be empty lines |
| 199 | # - we might have lesser indented code blocks at the end |
| 200 | if end is None: |
| 201 | end = len(source.lines) |
| 202 | |
| 203 | if end > start + 1: |
| 204 | # Make sure we don't span differently indented code blocks |
| 205 | # by using the BlockFinder helper used which inspect.getsource() uses itself. |
| 206 | block_finder = inspect.BlockFinder() |
| 207 | # If we start with an indented line, put blockfinder to "started" mode. |
| 208 | block_finder.started = ( |
| 209 | bool(source.lines[start]) and source.lines[start][0].isspace() |
| 210 | ) |
| 211 | it = ((x + "\n") for x in source.lines[start:end]) |
| 212 | try: |
| 213 | for tok in tokenize.generate_tokens(lambda: next(it)): |
| 214 | block_finder.tokeneater(*tok) |
| 215 | except (inspect.EndOfBlock, IndentationError): |
| 216 | end = block_finder.last + start |
| 217 | except Exception: |
| 218 | pass |
| 219 | |
| 220 | # The end might still point to a comment or empty line, correct it. |
| 221 | end = min(end, len(source.lines)) |
| 222 | while end: |
| 223 | line = source.lines[end - 1].lstrip() |
| 224 | if line.startswith("#") or not line: |
| 225 | end -= 1 |
| 226 | else: |
| 227 | break |
| 228 | return astnode, start, end |