Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 46 additions & 27 deletions src/Source/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,30 +36,9 @@ public function lines(): \Generator
}

/** @throws \OutOfBoundsException if the offset lies outside the source */
public function lineAtOffset(int $offset): Line
public function lineNumberAtOffset(int $offset): int
{
$sourceLength = strlen($this->content);
if ($offset < 0 || $offset > $sourceLength) {
throw new \OutOfBoundsException(
sprintf('Source offset %d is outside the valid range 0..%d.', $offset, $sourceLength),
);
}

$lineBeginOffsets = $this->lineBeginOffsets();
$low = 0;
$high = count($lineBeginOffsets) - 1;

while ($low < $high) {
$middle = intdiv($low + $high + 1, 2);

if ($lineBeginOffsets[$middle] <= $offset) {
$low = $middle;
} else {
$high = $middle - 1;
}
}

return $this->createLineAtOffset($low + 1, $lineBeginOffsets[$low]);
return $this->lineIndexAtOffset($offset, $this->lineBeginOffsets()) + 1;
}

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

$lineBeginOffsets = [0];
$sourceLength = strlen($this->content);
$lineBeginOffset = 0;

while ($lineBeginOffset < $sourceLength) {
$lineLength = strcspn($this->content, "\r\n", $lineBeginOffset);
$lineEndingOffset = $lineBeginOffset + $lineLength;

foreach ($this->lines() as $line) {
if ($line->number === 1) {
continue;
if ($lineEndingOffset === $sourceLength) {
break;
}

$lineBeginOffsets[] = $line->beginOffset;
$lineBeginOffset = $lineEndingOffset + (
$this->content[$lineEndingOffset] === "\r"
&& ($this->content[$lineEndingOffset + 1] ?? null) === "\n"
? 2
: 1
);
$lineBeginOffsets[] = $lineBeginOffset;
}

return $this->lineBeginOffsets = $lineBeginOffsets;
}

/**
* @param non-empty-list<int> $lineBeginOffsets
* @throws \OutOfBoundsException if the offset lies outside the source
*/
private function lineIndexAtOffset(int $offset, array $lineBeginOffsets): int
{
$sourceLength = strlen($this->content);
if ($offset < 0 || $offset > $sourceLength) {
throw new \OutOfBoundsException(
sprintf('Source offset %d is outside the valid range 0..%d.', $offset, $sourceLength),
);
}

$low = 0;
$high = count($lineBeginOffsets) - 1;

while ($low < $high) {
$middle = intdiv($low + $high + 1, 2);

if ($lineBeginOffsets[$middle] <= $offset) {
$low = $middle;
} else {
$high = $middle - 1;
}
}

return $low;
}

private function createLineAtOffset(int $lineNumber, int $lineBeginOffset): Line
{
$sourceLength = strlen($this->content);
Expand Down
2 changes: 1 addition & 1 deletion src/Violation/SourceRange.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
public static function fromFile(File $file, int $beginOffset, int $untilOffset): self
{
return new self(
line: $file->lineAtOffset($beginOffset)->number,
line: $file->lineNumberAtOffset($beginOffset),
beginOffset: $beginOffset,
untilOffset: $untilOffset,
content: substr($file->content, $beginOffset, $untilOffset - $beginOffset),
Expand Down
20 changes: 10 additions & 10 deletions tests/Unit/Source/FileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,17 @@ public function itRepresentsLinesAndTheirEndings(): void
}

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

self::assertSame(1, $file->lineAtOffset(0)->number);
self::assertSame(1, $file->lineAtOffset(3)->number);
self::assertSame(1, $file->lineAtOffset(4)->number);
self::assertSame(2, $file->lineAtOffset(5)->number);
self::assertSame(2, $file->lineAtOffset(8)->number);
self::assertSame(3, $file->lineAtOffset(9)->number);
self::assertSame(3, $file->lineAtOffset(14)->number);
self::assertSame(1, $file->lineNumberAtOffset(0));
self::assertSame(1, $file->lineNumberAtOffset(3));
self::assertSame(1, $file->lineNumberAtOffset(4));
self::assertSame(2, $file->lineNumberAtOffset(5));
self::assertSame(2, $file->lineNumberAtOffset(8));
self::assertSame(3, $file->lineNumberAtOffset(9));
self::assertSame(3, $file->lineNumberAtOffset(14));
}

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

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

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

$this->expectException(\OutOfBoundsException::class);
$file->lineAtOffset(4);
$file->lineNumberAtOffset(4);
}
}
2 changes: 0 additions & 2 deletions tests/Unit/Violation/AffectedRangesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace DocbookCS\Tests\Unit\Violation;

use DocbookCS\Source\File;
use DocbookCS\Source\Line;
use DocbookCS\Violation\Severity;
use DocbookCS\Violation\SourceRange;
use DocbookCS\Violation\Violation;
Expand All @@ -20,7 +19,6 @@
CoversClass(Violation::class),
//
UsesClass(File::class),
UsesClass(Line::class),
]
final class AffectedRangesTest extends TestCase
{
Expand Down