phplrt 4.0

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.

  • $line MUST be counted from the beginning of the source, starting at PositionInterface::MIN_LINE.
  • $column MUST be counted from the beginning of its own line, starting at PositionInterface::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 SourceExceptionInterface when 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.