-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathPosResolver.php
More file actions
121 lines (104 loc) · 3.53 KB
/
PosResolver.php
File metadata and controls
121 lines (104 loc) · 3.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
<?php
declare(strict_types=1);
namespace ScipPhp\Parser;
use InvalidArgumentException;
use PhpParser\Comment;
use PhpParser\Node;
use PhpParser\Node\Const_;
use PhpParser\Node\PropertyItem;
use RuntimeException;
use function explode;
use function preg_match;
use function preg_quote;
use function strlen;
use function strpos;
use function strrpos;
use function substr_count;
final readonly class PosResolver
{
private int $codeLen;
/** @param non-empty-string $code */
public function __construct(private string $code)
{
$this->codeLen = strlen($code);
}
/**
* Resolves the position of the node in the format
* [startLine, startCharacter, endLine, endCharacter].
* As defined by the SCIP schema, line numbers and characters are 0-based.
*
* @return array{int, int, int, int}
*/
public function pos(Node|Comment $n): array
{
return [
$n->getStartLine() - 1,
$this->toColumn($n->getStartFilePos()) - 1,
$n->getEndLine() - 1,
$this->toColumn($n->getEndFilePos()),
];
}
/**
* Resolves the enclosing range of a definition node, including
* any leading doc comment.
*
* @return array{int, int, int, int}
*/
public function enclosingRange(Node $n): array
{
// For Const_ and PropertyItem, the doc comment and full statement
// range live on the parent node (ClassConst / Property).
$node = $n;
if (($n instanceof Const_ || $n instanceof PropertyItem) && $n->getAttribute('parent') instanceof Node) {
$node = $n->getAttribute('parent');
}
$doc = $node->getDocComment();
if ($doc !== null) {
return [
$doc->getStartLine() - 1,
$this->toColumn($doc->getStartFilePos()) - 1,
$node->getEndLine() - 1,
$this->toColumn($node->getEndFilePos()),
];
}
return $this->pos($node);
}
/**
* @param non-empty-string $tagName
* @param non-empty-string $name
* @return array{int, int, int, int}
*/
public static function posInDoc(string $doc, int $startLine, string $tagName, string $name): array
{
$quotedName = preg_quote($name);
$pattern = "/^\s*(\/)?\*+\s*{$tagName}[\s\S]*{$quotedName}($|\(|\s+)/m";
if (preg_match($pattern, $doc) !== 1) {
throw new RuntimeException("Cannot find {$tagName} {$name} in doc comment: {$doc}.");
}
$index = strpos($doc, $name);
if ($index === false) {
throw new RuntimeException("Cannot find {$tagName} {$name} in doc comment: {$doc}.");
}
$line = substr_count($doc, "\n", 0, $index);
$lines = explode("\n", $doc);
$startColumn = strpos($lines[$line] ?? '', $name);
if ($startColumn === false) {
throw new RuntimeException("Cannot find {$tagName} {$name} on line {$line} in doc comment: {$doc}.");
}
$endColumn = $startColumn + strlen($name);
$startLine += $line;
return [$startLine, $startColumn, $startLine, $endColumn];
}
private function toColumn(int $filePos): int
{
$offset = $filePos - $this->codeLen;
if ($offset > 0) {
throw new InvalidArgumentException('Invalid position information.');
}
$lineStartPos = strrpos($this->code, "\n", $offset);
if ($lineStartPos === false) {
$lineStartPos = -1;
}
return $filePos - $lineStartPos;
}
}