Position Contracts
This document describes a common interface for a human-readable location inside a source - a line and a column - together with the interface for converting between such a location and an offset in bytes.
The goal is to let anything that talks about places in a source - an error reporter, an IDE plugin, a linter, a diff tool - speak of them in the same terms.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
1. Specification
1.1 Position
A position is an object implementing PositionInterface.
-
$lineMUST be counted from the beginning of the source, starting atPositionInterface::MIN_LINE. -
$columnMUST be counted from the beginning of its own line, starting atPositionInterface::MIN_COLUMN. - Both minimums are
1: the counting is one-based, not zero-based. - An implementation MUST be immutable.
Code that clamps or compares positions SHOULD use the constants rather than a
literal 1:
$line = \max(PositionInterface::MIN_LINE, $line - $context);
1.2 Factory
A position factory is an object implementing PositionFactoryInterface. It
converts in both directions, and both directions need the source, since the
answer is a property of the bytes in it.
-
createFromOffset()returns the position of the given offset, counted in bytes from the beginning of the given source. - An offset beyond the end of the source MUST be corrected to the end of it.
-
createOffsetFromPosition()returns the offset the given position points at. - A position beyond the end of its own line MUST be corrected to the end of that line, and a position beyond the end of the source MUST be corrected to the end of the source.
- Both MAY raise
SourceExceptionInterfacewhen the data of the source cannot be read.
Neither method fails on an argument that is out of range - both clamp. That is what makes the pair safe to use on numbers that have been arrived at by arithmetic, which is what an error reporter drawing a caret under a token does:
1use Phplrt\Contracts\Position\PositionFactoryInterface; 2use Phplrt\Contracts\Source\FileInterface; 3 4function report(PositionFactoryInterface $factory, FileInterface $source, int $offset): string 5{ 6 $position = $factory->createFromOffset($source, $offset); 7 8 return \sprintf('%s:%d:%d', $source->pathname, $position->line, $position->column); 9}
The factory raises SourceExceptionInterface rather than an exception of its
own, so locating something inside a source fails the same way reading it does.
2. Package
The interfaces described here are provided as part of two packages:
composer require phplrt/position-contracts composer require phplrt/position-factory-contracts
| Interface | Package |
|---|---|
PositionInterface |
position-contracts |
PositionFactoryInterface |
position-factory-contracts |
Both use the Phplrt\Contracts\Position namespace; the split is about what a
consumer depends on. phplrt/position-contracts requires nothing but PHP - a
line and a column mean nothing about where they came from - while
phplrt/position-factory-contracts requires it along with
phplrt/source-contracts, since counting lines means
reading a source.
A package implementing them provides the corresponding virtual package:
composer require phplrt/position-contracts-implementation composer require phplrt/position-factory-contracts-implementation
phplrt/position provides both, as Phplrt\Position\Position and
Phplrt\Position\PositionFactory.
Both packages ship a test declaring an anonymous implementation of every interface in them. Changing that test is allowed only in a major release, so no member can be added to either interface in a minor version.
3. Interfaces
3.1 Phplrt\Contracts\Position\PositionInterface
1<?php 2 3declare(strict_types=1); 4 5namespace Phplrt\Contracts\Position; 6 7/** 8 * A human-readable location inside a source. 9 * 10 * The line of a position MUST be counted from the beginning of the source and 11 * the column MUST be counted from the beginning of its own line, both starting 12 * at one. 13 * 14 * An implementation MUST be immutable. 15 * 16 * @readonly 17 */ 18interface PositionInterface 19{ 20 /** 21 * The minimal line number a position is allowed to have. 22 * 23 * @var int<1, max> 24 */ 25 public const int MIN_LINE = 1; 26 27 /** 28 * The minimal column number a position is allowed to have. 29 * 30 * @var int<1, max> 31 */ 32 public const int MIN_COLUMN = 1; 33 34 /** 35 * The number of the source line the position points at. 36 * 37 * @var int<1, max> 38 */ 39 public int $line { 40 get; 41 } 42 43 /** 44 * The number of the column within its own line the position points at. 45 * 46 * @var int<1, max> 47 */ 48 public int $column { 49 get; 50 } 51}
3.2 Phplrt\Contracts\Position\PositionFactoryInterface
1<?php 2 3declare(strict_types=1); 4 5namespace Phplrt\Contracts\Position; 6 7use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; 8use Phplrt\Contracts\Source\ReadableInterface; 9 10/** 11 * Converts the offsets inside a source into the positions and back. 12 */ 13interface PositionFactoryInterface 14{ 15 /** 16 * Creates the position of the given offset inside the given source. 17 * 18 * An offset beyond the end of the source MUST be corrected to the end 19 * of it. 20 * 21 * @param int<0, max> $offset the offset in bytes from the beginning of 22 * the source 23 * @return PositionInterface the position of the given offset 24 * @throws SourceExceptionInterface if the data of the given source cannot 25 * be read 26 */ 27 public function createFromOffset(ReadableInterface $source, int $offset): PositionInterface; 28 29 /** 30 * Creates the offset in bytes from the beginning of the given source the 31 * given position points at. 32 * 33 * A position beyond the end of its own line MUST be corrected to the end 34 * of that line, and a position beyond the end of the source MUST be 35 * corrected to the end of the source. 36 * 37 * @return int<0, max> the offset the given position points at 38 * @throws SourceExceptionInterface if the data of the given source cannot 39 * be read 40 */ 41 public function createOffsetFromPosition(ReadableInterface $source, PositionInterface $position): int; 42}
4. See Also
- Usage - the phplrt implementation, and what it does about a large source.
- Source Contracts - what a position is calculated over.