Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/black/ranges.py: 14%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

260 statements  

1"""Functions related to Black's formatting by line ranges feature.""" 

2 

3import difflib 

4from collections.abc import Collection, Iterator, Sequence 

5from dataclasses import dataclass 

6 

7from black.nodes import ( 

8 LN, 

9 STANDALONE_COMMENT, 

10 Leaf, 

11 Node, 

12 Visitor, 

13 first_leaf, 

14 furthest_ancestor_with_last_leaf, 

15 last_leaf, 

16 syms, 

17) 

18from blib2to3.pgen2.token import ASYNC, NEWLINE 

19 

20 

21def parse_line_ranges(line_ranges: Sequence[str]) -> list[tuple[int, int]]: 

22 lines: list[tuple[int, int]] = [] 

23 for lines_str in line_ranges: 

24 parts = lines_str.split("-") 

25 if len(parts) != 2: 

26 raise ValueError( 

27 "Incorrect --line-ranges format, expect 'START-END', found" 

28 f" {lines_str!r}" 

29 ) 

30 try: 

31 start = int(parts[0]) 

32 end = int(parts[1]) 

33 except ValueError: 

34 raise ValueError( 

35 "Incorrect --line-ranges value, expect integer ranges, found" 

36 f" {lines_str!r}" 

37 ) from None 

38 else: 

39 if start < 1 or end < 1: 

40 raise ValueError( 

41 "Incorrect --line-ranges value, " 

42 f"expect positive integers, found {lines_str!r}" 

43 ) 

44 if start > end: 

45 raise ValueError( 

46 "Incorrect --line-ranges value, expect START <= END, " 

47 f"found {lines_str!r}" 

48 ) 

49 lines.append((start, end)) 

50 return lines 

51 

52 

53def is_valid_line_range(lines: tuple[int, int]) -> bool: 

54 """Returns whether the line range is valid.""" 

55 return not lines or lines[0] <= lines[1] 

56 

57 

58def sanitized_lines( 

59 lines: Collection[tuple[int, int]], src_contents: str 

60) -> Collection[tuple[int, int]]: 

61 """Returns the valid line ranges for the given source. 

62 

63 This removes ranges that are entirely outside the valid lines. 

64 

65 Other ranges are normalized so that the start values are at least 1 and the 

66 end values are at most the (1-based) index of the last source line. 

67 """ 

68 if not src_contents: 

69 return [] 

70 good_lines = [] 

71 src_line_count = src_contents.count("\n") 

72 if not src_contents.endswith("\n"): 

73 src_line_count += 1 

74 for start, end in lines: 

75 if start > src_line_count: 

76 continue 

77 # line-ranges are 1-based 

78 start = max(start, 1) 

79 if end < start: 

80 continue 

81 end = min(end, src_line_count) 

82 good_lines.append((start, end)) 

83 return good_lines 

84 

85 

86def adjusted_lines( 

87 lines: Collection[tuple[int, int]], 

88 original_source: str, 

89 modified_source: str, 

90) -> list[tuple[int, int]]: 

91 """Returns the adjusted line ranges based on edits from the original code. 

92 

93 This computes the new line ranges by diffing original_source and 

94 modified_source, and adjust each range based on how the range overlaps with 

95 the diffs. 

96 

97 Note the diff can contain lines outside of the original line ranges. This can 

98 happen when the formatting has to be done in adjacent to maintain consistent 

99 local results. For example: 

100 

101 1. def my_func(arg1, arg2, 

102 2. arg3,): 

103 3. pass 

104 

105 If it restricts to line 2-2, it can't simply reformat line 2, it also has 

106 to reformat line 1: 

107 

108 1. def my_func( 

109 2. arg1, 

110 3. arg2, 

111 4. arg3, 

112 5. ): 

113 6. pass 

114 

115 In this case, we will expand the line ranges to also include the whole diff 

116 block. 

117 

118 Args: 

119 lines: a collection of line ranges. 

120 original_source: the original source. 

121 modified_source: the modified source. 

122 """ 

123 lines_mappings = _calculate_lines_mappings(original_source, modified_source) 

124 

125 new_lines = [] 

126 # Keep an index of the current search. Since the lines and lines_mappings are 

127 # sorted, this makes the search complexity linear. 

128 current_mapping_index = 0 

129 for start, end in sorted(lines): 

130 start_mapping_index = _find_lines_mapping_index( 

131 start, 

132 lines_mappings, 

133 current_mapping_index, 

134 ) 

135 end_mapping_index = _find_lines_mapping_index( 

136 end, 

137 lines_mappings, 

138 start_mapping_index, 

139 ) 

140 current_mapping_index = start_mapping_index 

141 if start_mapping_index >= len(lines_mappings) or end_mapping_index >= len( 

142 lines_mappings 

143 ): 

144 # Protect against invalid inputs. 

145 continue 

146 start_mapping = lines_mappings[start_mapping_index] 

147 end_mapping = lines_mappings[end_mapping_index] 

148 if start_mapping.is_changed_block: 

149 # When the line falls into a changed block, expands to the whole block. 

150 new_start = start_mapping.modified_start 

151 else: 

152 new_start = ( 

153 start - start_mapping.original_start + start_mapping.modified_start 

154 ) 

155 if end_mapping.is_changed_block: 

156 # When the line falls into a changed block, expands to the whole block. 

157 new_end = end_mapping.modified_end 

158 else: 

159 new_end = end - end_mapping.original_start + end_mapping.modified_start 

160 new_range = (new_start, new_end) 

161 if is_valid_line_range(new_range): 

162 new_lines.append(new_range) 

163 return new_lines 

164 

165 

166def convert_unchanged_lines(src_node: Node, lines: Collection[tuple[int, int]]) -> None: 

167 r"""Converts unchanged lines to STANDALONE_COMMENT. 

168 

169 The idea is similar to how `# fmt: on/off` is implemented. It also converts the 

170 nodes between those markers as a single `STANDALONE_COMMENT` leaf node with 

171 the unformatted code as its value. `STANDALONE_COMMENT` is a "fake" token 

172 that will be formatted as-is with its prefix normalized. 

173 

174 Here we perform two passes: 

175 

176 1. Visit the top-level statements, and convert them to a single 

177 `STANDALONE_COMMENT` when unchanged. This speeds up formatting when some 

178 of the top-level statements aren't changed. 

179 2. Convert unchanged "unwrapped lines" to `STANDALONE_COMMENT` nodes line by 

180 line. "unwrapped lines" are divided by the `NEWLINE` token. e.g. a 

181 multi-line statement is *one* "unwrapped line" that ends with `NEWLINE`, 

182 even though this statement itself can span multiple lines, and the 

183 tokenizer only sees the last '\n' as the `NEWLINE` token. 

184 

185 NOTE: During pass (2), comment prefixes and indentations are ALWAYS 

186 normalized even when the lines aren't changed. This is fixable by moving 

187 more formatting to pass (1). However, it's hard to get it correct when 

188 incorrect indentations are used. So we defer this to future optimizations. 

189 """ 

190 lines_set: set[int] = set() 

191 for start, end in lines: 

192 lines_set.update(range(start, end + 1)) 

193 replacements = _NodeReplacements() 

194 visitor = _TopLevelStatementsVisitor(lines_set, replacements) 

195 _ = list(visitor.visit(src_node)) # Consume all results. 

196 replacements.apply() 

197 _convert_unchanged_line_by_line(src_node, lines_set) 

198 

199 

200class _NodeReplacements: 

201 """Collects STANDALONE_COMMENT conversions and applies them per parent. 

202 

203 Converting a node splices a `STANDALONE_COMMENT` leaf in place of a run of 

204 sibling nodes. Doing that one node at a time with `Base.remove` / 

205 `insert_child` scans and shifts the parent's children list on every call, so 

206 converting the many sibling blocks of one parent (a long if/elif chain, a 

207 match with many cases, a module with many top-level statements) is O(n^2). 

208 Recording the conversions and rewriting each parent's children in a single 

209 pass makes it O(n). 

210 """ 

211 

212 def __init__(self) -> None: 

213 # id(parent) -> (parent, {id(child): standalone to splice in}, {ids to drop}) 

214 # A run's nodes can live under more than one parent (an `async` statement 

215 # keeps the `ASYNC` leaf on the grandparent), so the standalone replaces 

216 # the run's first node under its own parent while the remaining nodes are 

217 # just dropped from wherever they sit. 

218 self._by_parent: dict[int, tuple[Node, dict[int, Leaf], set[int]]] = {} 

219 # NEWLINE leaves already inside a recorded run, so the line-by-line pass 

220 # doesn't record an overlapping conversion for them. 

221 self.covered_newlines: set[int] = set() 

222 # Nodes already scheduled for removal. When conversions immediately 

223 # mutated the tree, a second conversion touching an already-removed node 

224 # was a no-op (`Base.remove` returned None); recording defers the 

225 # mutation, so we skip such nodes explicitly to keep that behaviour. 

226 self._recorded: set[int] = set() 

227 # A leaf's prefix (indentation, blank lines, comments) moves onto its 

228 # STANDALONE_COMMENT. When an inner and an outer node that share the same 

229 # first leaf are both converted (a decorator inside a decorated block), 

230 # the earlier conversion clears the prefix off the leaf, so the later one 

231 # would see an empty prefix. Remember the taken prefix keyed by the leaf 

232 # so both conversions reuse the original. 

233 self._taken_prefixes: dict[int, str] = {} 

234 

235 def _entry(self, parent: Node) -> tuple[Node, dict[int, Leaf], set[int]]: 

236 return self._by_parent.setdefault(id(parent), (parent, {}, set())) 

237 

238 def take_prefix(self, first: Leaf) -> str: 

239 cached = self._taken_prefixes.get(id(first)) 

240 if cached is not None: 

241 return cached 

242 prefix = first.prefix 

243 first.prefix = "" 

244 # Immediate conversion replaced the whole run with a freshly built 

245 # STANDALONE_COMMENT leaf, whose synthesized position is line 0. Recording 

246 # the conversion leaves the original leaf in place, so mirror that line 0 

247 # here: when a later, enclosing node shares this first leaf (a decorated 

248 # block over an already-recorded decorator), _get_line_range sees the run 

249 # as starting at line 0, matching the immediate behaviour. 

250 first.lineno = 0 

251 self._taken_prefixes[id(first)] = prefix 

252 return prefix 

253 

254 def is_recorded(self, node: LN) -> bool: 

255 return id(node) in self._recorded 

256 

257 def record(self, run: list[LN], standalone: Leaf) -> None: 

258 first = run[0] 

259 first_parent = first.parent 

260 if first_parent is None or id(first) in self._recorded: 

261 return 

262 for node in run: 

263 self._recorded.add(id(node)) 

264 for leaf in node.leaves(): 

265 if leaf.type == NEWLINE: 

266 self.covered_newlines.add(id(leaf)) 

267 parent = node.parent 

268 if parent is not None: 

269 self._entry(parent)[2].add(id(node)) 

270 self._entry(first_parent)[1][id(first)] = standalone 

271 

272 def apply(self) -> None: 

273 for parent, replaced, removed in self._by_parent.values(): 

274 new_children: list[LN] = [] 

275 for child in parent.children: 

276 standalone = replaced.get(id(child)) 

277 if standalone is not None: 

278 standalone.parent = parent 

279 new_children.append(standalone) 

280 child.parent = None 

281 elif id(child) in removed: 

282 child.parent = None 

283 else: 

284 new_children.append(child) 

285 parent.children = new_children 

286 parent.changed() 

287 parent.invalidate_sibling_maps() 

288 

289 

290def _contains_standalone_comment(node: LN) -> bool: 

291 if isinstance(node, Leaf): 

292 return node.type == STANDALONE_COMMENT 

293 else: 

294 for child in node.children: 

295 if _contains_standalone_comment(child): 

296 return True 

297 return False 

298 

299 

300class _TopLevelStatementsVisitor(Visitor[None]): 

301 """ 

302 A node visitor that converts unchanged top-level statements to 

303 STANDALONE_COMMENT. 

304 

305 This is used in addition to _convert_unchanged_line_by_line, to 

306 speed up formatting when there are unchanged top-level 

307 classes/functions/statements. 

308 """ 

309 

310 def __init__(self, lines_set: set[int], replacements: "_NodeReplacements"): 

311 self._lines_set = lines_set 

312 self._replacements = replacements 

313 

314 def visit_simple_stmt(self, node: Node) -> Iterator[None]: 

315 # This is only called for top-level statements, since `visit_suite` 

316 # won't visit its children nodes. 

317 yield from [] 

318 newline_leaf = last_leaf(node) 

319 if not newline_leaf: 

320 return 

321 assert ( 

322 newline_leaf.type == NEWLINE 

323 ), f"Unexpectedly found leaf.type={newline_leaf.type}" 

324 # We need to find the furthest ancestor with the NEWLINE as the last 

325 # leaf, since a `suite` can simply be a `simple_stmt` when it puts 

326 # its body on the same line. Example: `if cond: pass`. 

327 ancestor = furthest_ancestor_with_last_leaf(newline_leaf) 

328 if not _get_line_range(ancestor).intersection(self._lines_set): 

329 _convert_node_to_standalone_comment(ancestor, self._replacements) 

330 

331 def visit_suite(self, node: Node) -> Iterator[None]: 

332 yield from [] 

333 # If there is a STANDALONE_COMMENT node, it means parts of the node tree 

334 # have fmt on/off/skip markers. Those STANDALONE_COMMENT nodes can't 

335 # be simply converted by calling str(node). So we just don't convert 

336 # here. 

337 if _contains_standalone_comment(node): 

338 return 

339 # Find the semantic parent of this suite. For `async_stmt` and 

340 # `async_funcdef`, the ASYNC token is defined on a separate level by the 

341 # grammar. 

342 semantic_parent = node.parent 

343 if semantic_parent is not None: 

344 if ( 

345 semantic_parent.prev_sibling is not None 

346 and semantic_parent.prev_sibling.type == ASYNC 

347 ): 

348 semantic_parent = semantic_parent.parent 

349 if semantic_parent is not None and not _get_line_range( 

350 semantic_parent 

351 ).intersection(self._lines_set): 

352 _convert_node_to_standalone_comment(semantic_parent, self._replacements) 

353 

354 

355def _convert_unchanged_line_by_line(node: Node, lines_set: set[int]) -> None: 

356 """Converts unchanged to STANDALONE_COMMENT line by line.""" 

357 replacements = _NodeReplacements() 

358 for leaf in node.leaves(): 

359 if leaf.type != NEWLINE: 

360 # We only consider "unwrapped lines", which are divided by the NEWLINE 

361 # token. 

362 continue 

363 if id(leaf) in replacements.covered_newlines: 

364 # This NEWLINE is inside a run already scheduled for conversion (e.g. 

365 # a second decorator on a stacked decorator block). 

366 continue 

367 if leaf.parent and leaf.parent.type == syms.match_stmt: 

368 # The `suite` node is defined as: 

369 # match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT 

370 # Here we need to check `subject_expr`. The `case_block+` will be 

371 # checked by their own NEWLINEs. 

372 nodes_to_ignore: list[LN] = [] 

373 prev_sibling = leaf.prev_sibling 

374 while prev_sibling: 

375 nodes_to_ignore.insert(0, prev_sibling) 

376 prev_sibling = prev_sibling.prev_sibling 

377 if not _get_line_range(nodes_to_ignore).intersection(lines_set): 

378 _convert_nodes_to_standalone_comment( 

379 nodes_to_ignore, newline=leaf, replacements=replacements 

380 ) 

381 elif leaf.parent and leaf.parent.type == syms.suite: 

382 # The `suite` node is defined as: 

383 # suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT 

384 # We will check `simple_stmt` and `stmt+` separately against the lines set 

385 parent_sibling = leaf.parent.prev_sibling 

386 nodes_to_ignore = [] 

387 while parent_sibling and parent_sibling.type != syms.suite: 

388 # NOTE: Multiple suite nodes can exist as siblings in e.g. `if_stmt`. 

389 nodes_to_ignore.insert(0, parent_sibling) 

390 parent_sibling = parent_sibling.prev_sibling 

391 # Special case for `async_stmt` and `async_funcdef` where the ASYNC 

392 # token is on the grandparent node. 

393 grandparent = leaf.parent.parent 

394 if ( 

395 grandparent is not None 

396 and grandparent.prev_sibling is not None 

397 and grandparent.prev_sibling.type == ASYNC 

398 ): 

399 nodes_to_ignore.insert(0, grandparent.prev_sibling) 

400 if not _get_line_range(nodes_to_ignore).intersection(lines_set): 

401 _convert_nodes_to_standalone_comment( 

402 nodes_to_ignore, newline=leaf, replacements=replacements 

403 ) 

404 else: 

405 ancestor = furthest_ancestor_with_last_leaf(leaf) 

406 # Consider multiple decorators as a whole block, as their 

407 # newlines have different behaviors than the rest of the grammar. 

408 if ( 

409 ancestor.type == syms.decorator 

410 and ancestor.parent 

411 and ancestor.parent.type == syms.decorators 

412 ): 

413 ancestor = ancestor.parent 

414 if not _get_line_range(ancestor).intersection(lines_set): 

415 _convert_node_to_standalone_comment(ancestor, replacements) 

416 replacements.apply() 

417 

418 

419def _convert_node_to_standalone_comment( 

420 node: LN, replacements: "_NodeReplacements" 

421) -> None: 

422 """Convert node to STANDALONE_COMMENT by modifying the tree inline.""" 

423 parent = node.parent 

424 if not parent or replacements.is_recorded(node): 

425 return 

426 first = first_leaf(node) 

427 last = last_leaf(node) 

428 if not first or not last: 

429 return 

430 if first is last: 

431 # This can happen on the following edge cases: 

432 # 1. A block of `# fmt: off/on` code except the `# fmt: on` is placed 

433 # on the end of the last line instead of on a new line. 

434 # 2. A single backslash on its own line followed by a comment line. 

435 # Ideally we don't want to format them when not requested, but fixing 

436 # isn't easy. These cases are also badly formatted code, so it isn't 

437 # too bad we reformat them. 

438 return 

439 # The prefix contains comments and indentation whitespaces. They are 

440 # reformatted accordingly to the correct indentation level. 

441 # This also means the indentation will be changed on the unchanged lines, and 

442 # this is actually required to not break incremental reformatting. 

443 prefix = replacements.take_prefix(first) 

444 # For a single-line decorated item the decorator and the item need a newline 

445 # between them. The conversions are recorded and applied in a single pass 

446 # instead of mutating the tree per node, so the decorator here is still its 

447 # own child node carrying its trailing NEWLINE, which already separates it 

448 # from the item; there's nothing to add back. (A decorated item spanning 

449 # multiple lines is handled by the earlier suite case in 

450 # _convert_unchanged_line_by_line, which manages the newlines itself.) 

451 # Remove the '\n', as STANDALONE_COMMENT will have '\n' appended when 

452 # generating the formatted code. 

453 value = str(node)[:-1] 

454 replacements.record( 

455 [node], 

456 Leaf( 

457 STANDALONE_COMMENT, 

458 value, 

459 prefix=prefix, 

460 fmt_pass_converted_first_leaf=first, 

461 ), 

462 ) 

463 

464 

465def _convert_nodes_to_standalone_comment( 

466 nodes: Sequence[LN], *, newline: Leaf, replacements: "_NodeReplacements" 

467) -> None: 

468 """Convert nodes to STANDALONE_COMMENT by modifying the tree inline.""" 

469 if not nodes: 

470 return 

471 parent = nodes[0].parent 

472 first = first_leaf(nodes[0]) 

473 if not parent or not first or replacements.is_recorded(nodes[0]): 

474 return 

475 prefix = replacements.take_prefix(first) 

476 value = "".join(str(node) for node in nodes) 

477 # The prefix comment on the NEWLINE leaf is the trailing comment of the statement. 

478 if newline.prefix: 

479 value += newline.prefix 

480 newline.prefix = "" 

481 replacements.record( 

482 list(nodes), 

483 Leaf( 

484 STANDALONE_COMMENT, 

485 value, 

486 prefix=prefix, 

487 fmt_pass_converted_first_leaf=first, 

488 ), 

489 ) 

490 

491 

492def _leaf_line_end(leaf: Leaf) -> int: 

493 """Returns the line number of the leaf node's last line.""" 

494 if leaf.type == NEWLINE: 

495 return leaf.lineno 

496 else: 

497 # Leaf nodes like multiline strings can occupy multiple lines. 

498 return leaf.lineno + str(leaf).count("\n") 

499 

500 

501def _get_line_range(node_or_nodes: LN | list[LN]) -> set[int]: 

502 """Returns the line range of this node or list of nodes.""" 

503 if isinstance(node_or_nodes, list): 

504 nodes = node_or_nodes 

505 if not nodes: 

506 return set() 

507 first = first_leaf(nodes[0]) 

508 last = last_leaf(nodes[-1]) 

509 if first and last: 

510 line_start = first.lineno 

511 line_end = _leaf_line_end(last) 

512 return set(range(line_start, line_end + 1)) 

513 else: 

514 return set() 

515 else: 

516 node = node_or_nodes 

517 if isinstance(node, Leaf): 

518 return set(range(node.lineno, _leaf_line_end(node) + 1)) 

519 else: 

520 first = first_leaf(node) 

521 last = last_leaf(node) 

522 if first and last: 

523 return set(range(first.lineno, _leaf_line_end(last) + 1)) 

524 else: 

525 return set() 

526 

527 

528@dataclass 

529class _LinesMapping: 

530 """1-based lines mapping from original source to modified source. 

531 

532 Lines [original_start, original_end] from original source 

533 are mapped to [modified_start, modified_end]. 

534 

535 The ranges are inclusive on both ends. 

536 """ 

537 

538 original_start: int 

539 original_end: int 

540 modified_start: int 

541 modified_end: int 

542 # Whether this range corresponds to a changed block, or an unchanged block. 

543 is_changed_block: bool 

544 

545 

546def _calculate_lines_mappings( 

547 original_source: str, 

548 modified_source: str, 

549) -> Sequence[_LinesMapping]: 

550 """Returns a sequence of _LinesMapping by diffing the sources. 

551 

552 For example, given the following diff: 

553 import re 

554 - def func(arg1, 

555 - arg2, arg3): 

556 + def func(arg1, arg2, arg3): 

557 pass 

558 It returns the following mappings: 

559 original -> modified 

560 (1, 1) -> (1, 1), is_changed_block=False (the "import re" line) 

561 (2, 3) -> (2, 2), is_changed_block=True (the diff) 

562 (4, 4) -> (3, 3), is_changed_block=False (the "pass" line) 

563 

564 You can think of this visually as if it brings up a side-by-side diff, and tries 

565 to map the line ranges from the left side to the right side: 

566 

567 (1, 1)->(1, 1) 1. import re 1. import re 

568 (2, 3)->(2, 2) 2. def func(arg1, 2. def func(arg1, arg2, arg3): 

569 3. arg2, arg3): 

570 (4, 4)->(3, 3) 4. pass 3. pass 

571 

572 Args: 

573 original_source: the original source. 

574 modified_source: the modified source. 

575 """ 

576 matcher = difflib.SequenceMatcher( 

577 None, 

578 original_source.splitlines(keepends=True), 

579 modified_source.splitlines(keepends=True), 

580 ) 

581 matching_blocks = matcher.get_matching_blocks() 

582 lines_mappings: list[_LinesMapping] = [] 

583 # matching_blocks is a sequence of "same block of code ranges", see 

584 # https://docs.python.org/3/library/difflib.html#difflib.SequenceMatcher.get_matching_blocks 

585 # Each block corresponds to a _LinesMapping with is_changed_block=False, 

586 # and the ranges between two blocks corresponds to a _LinesMapping with 

587 # is_changed_block=True, 

588 # NOTE: matching_blocks is 0-based, but _LinesMapping is 1-based. 

589 for i, block in enumerate(matching_blocks): 

590 if i == 0: 

591 if block.a != 0 or block.b != 0: 

592 lines_mappings.append( 

593 _LinesMapping( 

594 original_start=1, 

595 original_end=block.a, 

596 modified_start=1, 

597 modified_end=block.b, 

598 is_changed_block=False, 

599 ) 

600 ) 

601 else: 

602 previous_block = matching_blocks[i - 1] 

603 lines_mappings.append( 

604 _LinesMapping( 

605 original_start=previous_block.a + previous_block.size + 1, 

606 original_end=block.a, 

607 modified_start=previous_block.b + previous_block.size + 1, 

608 modified_end=block.b, 

609 is_changed_block=True, 

610 ) 

611 ) 

612 if i < len(matching_blocks) - 1: 

613 lines_mappings.append( 

614 _LinesMapping( 

615 original_start=block.a + 1, 

616 original_end=block.a + block.size, 

617 modified_start=block.b + 1, 

618 modified_end=block.b + block.size, 

619 is_changed_block=False, 

620 ) 

621 ) 

622 return lines_mappings 

623 

624 

625def _find_lines_mapping_index( 

626 original_line: int, 

627 lines_mappings: Sequence[_LinesMapping], 

628 start_index: int, 

629) -> int: 

630 """Returns the original index of the lines mappings for the original line.""" 

631 index = start_index 

632 while index < len(lines_mappings): 

633 mapping = lines_mappings[index] 

634 if mapping.original_start <= original_line <= mapping.original_end: 

635 return index 

636 index += 1 

637 return index