Skip to content

Commit ff2f58d

Browse files
committed
perf: optimised source range resolving
1 parent ed3d822 commit ff2f58d

4 files changed

Lines changed: 57 additions & 40 deletions

File tree

src/Source/File.php

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,9 @@ public function lines(): \Generator
3636
}
3737

3838
/** @throws \OutOfBoundsException if the offset lies outside the source */
39-
public function lineAtOffset(int $offset): Line
39+
public function lineNumberAtOffset(int $offset): int
4040
{
41-
$sourceLength = strlen($this->content);
42-
if ($offset < 0 || $offset > $sourceLength) {
43-
throw new \OutOfBoundsException(
44-
sprintf('Source offset %d is outside the valid range 0..%d.', $offset, $sourceLength),
45-
);
46-
}
47-
48-
$lineBeginOffsets = $this->lineBeginOffsets();
49-
$low = 0;
50-
$high = count($lineBeginOffsets) - 1;
51-
52-
while ($low < $high) {
53-
$middle = intdiv($low + $high + 1, 2);
54-
55-
if ($lineBeginOffsets[$middle] <= $offset) {
56-
$low = $middle;
57-
} else {
58-
$high = $middle - 1;
59-
}
60-
}
61-
62-
return $this->createLineAtOffset($low + 1, $lineBeginOffsets[$low]);
41+
return $this->lineIndexAtOffset($offset, $this->lineBeginOffsets()) + 1;
6342
}
6443

6544
public function withContent(string $content): self
@@ -77,18 +56,58 @@ private function lineBeginOffsets(): array
7756
}
7857

7958
$lineBeginOffsets = [0];
59+
$sourceLength = strlen($this->content);
60+
$lineBeginOffset = 0;
61+
62+
while ($lineBeginOffset < $sourceLength) {
63+
$lineLength = strcspn($this->content, "\r\n", $lineBeginOffset);
64+
$lineEndingOffset = $lineBeginOffset + $lineLength;
8065

81-
foreach ($this->lines() as $line) {
82-
if ($line->number === 1) {
83-
continue;
66+
if ($lineEndingOffset === $sourceLength) {
67+
break;
8468
}
8569

86-
$lineBeginOffsets[] = $line->beginOffset;
70+
$lineBeginOffset = $lineEndingOffset + (
71+
$this->content[$lineEndingOffset] === "\r"
72+
&& ($this->content[$lineEndingOffset + 1] ?? null) === "\n"
73+
? 2
74+
: 1
75+
);
76+
$lineBeginOffsets[] = $lineBeginOffset;
8777
}
8878

8979
return $this->lineBeginOffsets = $lineBeginOffsets;
9080
}
9181

82+
/**
83+
* @param non-empty-list<int> $lineBeginOffsets
84+
* @throws \OutOfBoundsException if the offset lies outside the source
85+
*/
86+
private function lineIndexAtOffset(int $offset, array $lineBeginOffsets): int
87+
{
88+
$sourceLength = strlen($this->content);
89+
if ($offset < 0 || $offset > $sourceLength) {
90+
throw new \OutOfBoundsException(
91+
sprintf('Source offset %d is outside the valid range 0..%d.', $offset, $sourceLength),
92+
);
93+
}
94+
95+
$low = 0;
96+
$high = count($lineBeginOffsets) - 1;
97+
98+
while ($low < $high) {
99+
$middle = intdiv($low + $high + 1, 2);
100+
101+
if ($lineBeginOffsets[$middle] <= $offset) {
102+
$low = $middle;
103+
} else {
104+
$high = $middle - 1;
105+
}
106+
}
107+
108+
return $low;
109+
}
110+
92111
private function createLineAtOffset(int $lineNumber, int $lineBeginOffset): Line
93112
{
94113
$sourceLength = strlen($this->content);

src/Violation/SourceRange.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
public static function fromFile(File $file, int $beginOffset, int $untilOffset): self
1616
{
1717
return new self(
18-
line: $file->lineAtOffset($beginOffset)->number,
18+
line: $file->lineNumberAtOffset($beginOffset),
1919
beginOffset: $beginOffset,
2020
untilOffset: $untilOffset,
2121
content: substr($file->content, $beginOffset, $untilOffset - $beginOffset),

tests/Unit/Source/FileTest.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,17 @@ public function itRepresentsLinesAndTheirEndings(): void
3939
}
4040

4141
#[Test]
42-
public function itResolvesOffsetsToLines(): void
42+
public function itResolvesOffsetsToLineNumbers(): void
4343
{
4444
$file = new File('file.xml', "one\r\ntwo\nthree");
4545

46-
self::assertSame(1, $file->lineAtOffset(0)->number);
47-
self::assertSame(1, $file->lineAtOffset(3)->number);
48-
self::assertSame(1, $file->lineAtOffset(4)->number);
49-
self::assertSame(2, $file->lineAtOffset(5)->number);
50-
self::assertSame(2, $file->lineAtOffset(8)->number);
51-
self::assertSame(3, $file->lineAtOffset(9)->number);
52-
self::assertSame(3, $file->lineAtOffset(14)->number);
46+
self::assertSame(1, $file->lineNumberAtOffset(0));
47+
self::assertSame(1, $file->lineNumberAtOffset(3));
48+
self::assertSame(1, $file->lineNumberAtOffset(4));
49+
self::assertSame(2, $file->lineNumberAtOffset(5));
50+
self::assertSame(2, $file->lineNumberAtOffset(8));
51+
self::assertSame(3, $file->lineNumberAtOffset(9));
52+
self::assertSame(3, $file->lineNumberAtOffset(14));
5353
}
5454

5555
#[Test]
@@ -62,7 +62,7 @@ public function itIncludesAnEmptyLineAfterTheFinalLineEnding(): void
6262
new Line(2, '', '', 4),
6363
], iterator_to_array($file->lines()));
6464

65-
self::assertSame(2, $file->lineAtOffset(4)->number);
65+
self::assertSame(2, $file->lineNumberAtOffset(4));
6666
}
6767

6868
#[Test]
@@ -91,6 +91,6 @@ public function itRejectsOffsetsOutsideTheSource(): void
9191
$file = new File('file.xml', 'one');
9292

9393
$this->expectException(\OutOfBoundsException::class);
94-
$file->lineAtOffset(4);
94+
$file->lineNumberAtOffset(4);
9595
}
9696
}

tests/Unit/Violation/AffectedRangesTest.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace DocbookCS\Tests\Unit\Violation;
66

77
use DocbookCS\Source\File;
8-
use DocbookCS\Source\Line;
98
use DocbookCS\Violation\Severity;
109
use DocbookCS\Violation\SourceRange;
1110
use DocbookCS\Violation\Violation;
@@ -20,7 +19,6 @@
2019
CoversClass(Violation::class),
2120
//
2221
UsesClass(File::class),
23-
UsesClass(Line::class),
2422
]
2523
final class AffectedRangesTest extends TestCase
2624
{

0 commit comments

Comments
 (0)