Fix bin/publish: copy docs.dist from project root

Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

View File

@@ -0,0 +1,292 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\ParserConfig;
use function str_replace;
use function strtolower;
class ConstExprParser
{
private ParserConfig $config;
private bool $parseDoctrineStrings;
public function __construct(
ParserConfig $config
)
{
$this->config = $config;
$this->parseDoctrineStrings = false;
}
/**
* @internal
*/
public function toDoctrine(): self
{
$self = new self($this->config);
$self->parseDoctrineStrings = true;
return $self;
}
public function parse(TokenIterator $tokens): Ast\ConstExpr\ConstExprNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_FLOAT)) {
$value = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprFloatNode(str_replace('_', '', $value)),
$startLine,
$startIndex,
);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
$value = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $value)),
$startLine,
$startIndex,
);
}
if ($this->parseDoctrineStrings && $tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
$value = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($value)),
$startLine,
$startIndex,
);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
if ($this->parseDoctrineStrings) {
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_DOUBLE_QUOTED_STRING,
null,
$tokens->currentTokenLine(),
);
}
$value = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
$this->parseDoctrineString($value, $tokens),
$startLine,
$startIndex,
);
}
$value = StringUnescaper::unescapeString($tokens->currentTokenValue());
$type = $tokens->currentTokenType();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprStringNode(
$value,
$type === Lexer::TOKEN_SINGLE_QUOTED_STRING
? Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED
: Ast\ConstExpr\ConstExprStringNode::DOUBLE_QUOTED,
),
$startLine,
$startIndex,
);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
$identifier = $tokens->currentTokenValue();
$tokens->next();
switch (strtolower($identifier)) {
case 'true':
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprTrueNode(),
$startLine,
$startIndex,
);
case 'false':
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprFalseNode(),
$startLine,
$startIndex,
);
case 'null':
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprNullNode(),
$startLine,
$startIndex,
);
case 'array':
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES, $startIndex);
}
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$classConstantName = '';
$lastType = null;
while (true) {
if ($lastType !== Lexer::TOKEN_IDENTIFIER && $tokens->currentTokenType() === Lexer::TOKEN_IDENTIFIER) {
$classConstantName .= $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$lastType = Lexer::TOKEN_IDENTIFIER;
continue;
}
if ($lastType !== Lexer::TOKEN_WILDCARD && $tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
$classConstantName .= '*';
$lastType = Lexer::TOKEN_WILDCARD;
if ($tokens->getSkippedHorizontalWhiteSpaceIfAny() !== '') {
break;
}
continue;
}
if ($lastType === null) {
// trigger parse error if nothing valid was consumed
$tokens->consumeTokenType(Lexer::TOKEN_WILDCARD);
}
break;
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName),
$startLine,
$startIndex,
);
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstFetchNode('', $identifier),
$startLine,
$startIndex,
);
} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET, $startIndex);
}
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER,
null,
$tokens->currentTokenLine(),
);
}
private function parseArray(TokenIterator $tokens, int $endToken, int $startIndex): Ast\ConstExpr\ConstExprArrayNode
{
$items = [];
$startLine = $tokens->currentTokenLine();
if (!$tokens->tryConsumeTokenType($endToken)) {
do {
$items[] = $this->parseArrayItem($tokens);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA) && !$tokens->isCurrentTokenType($endToken));
$tokens->consumeTokenType($endToken);
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprArrayNode($items),
$startLine,
$startIndex,
);
}
/**
* This method is supposed to be called with TokenIterator after reading TOKEN_DOUBLE_QUOTED_STRING and shifting
* to the next token.
*/
public function parseDoctrineString(string $text, TokenIterator $tokens): Ast\ConstExpr\DoctrineConstExprStringNode
{
// Because of how Lexer works, a valid Doctrine string
// can consist of a sequence of TOKEN_DOUBLE_QUOTED_STRING and TOKEN_DOCTRINE_ANNOTATION_STRING
while ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING, Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
$text .= $tokens->currentTokenValue();
$tokens->next();
}
return new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($text));
}
private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprArrayItemNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$expr = $this->parse($tokens);
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_ARROW)) {
$key = $expr;
$value = $this->parse($tokens);
} else {
$key = null;
$value = $expr;
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprArrayItemNode($key, $value),
$startLine,
$startIndex,
);
}
/**
* @template T of Ast\ConstExpr\ConstExprNode
* @param T $node
* @return T
*/
private function enrichWithAttributes(TokenIterator $tokens, Ast\ConstExpr\ConstExprNode $node, int $startLine, int $startIndex): Ast\ConstExpr\ConstExprNode
{
if ($this->config->useLinesAttributes) {
$node->setAttribute(Ast\Attribute::START_LINE, $startLine);
$node->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
}
if ($this->config->useIndexAttributes) {
$node->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
$node->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
}
return $node;
}
}

View File

@@ -0,0 +1,100 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use Exception;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function assert;
use function json_encode;
use function sprintf;
use const JSON_INVALID_UTF8_SUBSTITUTE;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
class ParserException extends Exception
{
private string $currentTokenValue;
private int $currentTokenType;
private int $currentOffset;
private int $expectedTokenType;
private ?string $expectedTokenValue;
private ?int $currentTokenLine;
public function __construct(
string $currentTokenValue,
int $currentTokenType,
int $currentOffset,
int $expectedTokenType,
?string $expectedTokenValue,
?int $currentTokenLine
)
{
$this->currentTokenValue = $currentTokenValue;
$this->currentTokenType = $currentTokenType;
$this->currentOffset = $currentOffset;
$this->expectedTokenType = $expectedTokenType;
$this->expectedTokenValue = $expectedTokenValue;
$this->currentTokenLine = $currentTokenLine;
parent::__construct(sprintf(
'Unexpected token %s, expected %s%s at offset %d%s',
$this->formatValue($currentTokenValue),
Lexer::TOKEN_LABELS[$expectedTokenType],
$expectedTokenValue !== null ? sprintf(' (%s)', $this->formatValue($expectedTokenValue)) : '',
$currentOffset,
$currentTokenLine === null ? '' : sprintf(' on line %d', $currentTokenLine),
));
}
public function getCurrentTokenValue(): string
{
return $this->currentTokenValue;
}
public function getCurrentTokenType(): int
{
return $this->currentTokenType;
}
public function getCurrentOffset(): int
{
return $this->currentOffset;
}
public function getExpectedTokenType(): int
{
return $this->expectedTokenType;
}
public function getExpectedTokenValue(): ?string
{
return $this->expectedTokenValue;
}
public function getCurrentTokenLine(): ?int
{
return $this->currentTokenLine;
}
private function formatValue(string $value): string
{
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
assert($json !== false);
return $json;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use PHPStan\ShouldNotHappenException;
use function chr;
use function hexdec;
use function octdec;
use function preg_replace_callback;
use function str_replace;
use function substr;
class StringUnescaper
{
private const REPLACEMENTS = [
'\\' => '\\',
'n' => "\n",
'r' => "\r",
't' => "\t",
'f' => "\f",
'v' => "\v",
'e' => "\x1B",
];
public static function unescapeString(string $string): string
{
$quote = $string[0];
if ($quote === '\'') {
return str_replace(
['\\\\', '\\\''],
['\\', '\''],
substr($string, 1, -1),
);
}
return self::parseEscapeSequences(substr($string, 1, -1), '"');
}
/**
* Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L90-L130
*/
private static function parseEscapeSequences(string $str, string $quote): string
{
$str = str_replace('\\' . $quote, $quote, $str);
return preg_replace_callback(
'~\\\\([\\\\nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u\{([0-9a-fA-F]+)\})~',
static function ($matches) {
$str = $matches[1];
if (isset(self::REPLACEMENTS[$str])) {
return self::REPLACEMENTS[$str];
}
if ($str[0] === 'x' || $str[0] === 'X') {
return chr((int) hexdec(substr($str, 1)));
}
if ($str[0] === 'u') {
if (!isset($matches[2])) {
throw new ShouldNotHappenException();
}
return self::codePointToUtf8((int) hexdec($matches[2]));
}
return chr((int) octdec($str));
},
$str,
);
}
/**
* Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L132-L154
*/
private static function codePointToUtf8(int $num): string
{
if ($num <= 0x7F) {
return chr($num);
}
if ($num <= 0x7FF) {
return chr(($num >> 6) + 0xC0)
. chr(($num & 0x3F) + 0x80);
}
if ($num <= 0xFFFF) {
return chr(($num >> 12) + 0xE0)
. chr((($num >> 6) & 0x3F) + 0x80)
. chr(($num & 0x3F) + 0x80);
}
if ($num <= 0x1FFFFF) {
return chr(($num >> 18) + 0xF0)
. chr((($num >> 12) & 0x3F) + 0x80)
. chr((($num >> 6) & 0x3F) + 0x80)
. chr(($num & 0x3F) + 0x80);
}
// Invalid UTF-8 codepoint escape sequence: Codepoint too large
return "\xef\xbf\xbd";
}
}

View File

@@ -0,0 +1,428 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use LogicException;
use PHPStan\PhpDocParser\Ast\Comment;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function array_pop;
use function assert;
use function count;
use function in_array;
use function strlen;
use function substr;
class TokenIterator
{
/** @var list<array{string, int, int}> */
private array $tokens;
private int $index;
/** @var list<Comment> */
private array $comments = [];
/** @var list<array{int, list<Comment>}> */
private array $savePoints = [];
/** @var list<int> */
private array $skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS];
private ?string $newline = null;
/**
* @param list<array{string, int, int}> $tokens
*/
public function __construct(array $tokens, int $index = 0)
{
$this->tokens = $tokens;
$this->index = $index;
$this->skipIrrelevantTokens();
}
/**
* @return list<array{string, int, int}>
*/
public function getTokens(): array
{
return $this->tokens;
}
public function getContentBetween(int $startPos, int $endPos): string
{
if ($startPos < 0 || $endPos > count($this->tokens)) {
throw new LogicException();
}
$content = '';
for ($i = $startPos; $i < $endPos; $i++) {
$content .= $this->tokens[$i][Lexer::VALUE_OFFSET];
}
return $content;
}
public function getTokenCount(): int
{
return count($this->tokens);
}
public function currentTokenValue(): string
{
return $this->tokens[$this->index][Lexer::VALUE_OFFSET];
}
public function currentTokenType(): int
{
return $this->tokens[$this->index][Lexer::TYPE_OFFSET];
}
public function currentTokenOffset(): int
{
$offset = 0;
for ($i = 0; $i < $this->index; $i++) {
$offset += strlen($this->tokens[$i][Lexer::VALUE_OFFSET]);
}
return $offset;
}
public function currentTokenLine(): int
{
return $this->tokens[$this->index][Lexer::LINE_OFFSET];
}
public function currentTokenIndex(): int
{
return $this->index;
}
public function endIndexOfLastRelevantToken(): int
{
$endIndex = $this->currentTokenIndex();
$endIndex--;
while (in_array($this->tokens[$endIndex][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) {
if (!isset($this->tokens[$endIndex - 1])) {
break;
}
$endIndex--;
}
return $endIndex;
}
public function isCurrentTokenValue(string $tokenValue): bool
{
return $this->tokens[$this->index][Lexer::VALUE_OFFSET] === $tokenValue;
}
public function isCurrentTokenType(int ...$tokenType): bool
{
return in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true);
}
public function isPrecededByHorizontalWhitespace(): bool
{
return ($this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] ?? -1) === Lexer::TOKEN_HORIZONTAL_WS;
}
/**
* @throws ParserException
*/
public function consumeTokenType(int $tokenType): void
{
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) {
$this->throwError($tokenType);
}
if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) {
if ($this->newline === null) {
$this->detectNewline();
}
}
$this->next();
}
/**
* @throws ParserException
*/
public function consumeTokenValue(int $tokenType, string $tokenValue): void
{
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType || $this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) {
$this->throwError($tokenType, $tokenValue);
}
$this->next();
}
/** @phpstan-impure */
public function tryConsumeTokenValue(string $tokenValue): bool
{
if ($this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) {
return false;
}
$this->next();
return true;
}
/**
* @return list<Comment>
*/
public function flushComments(): array
{
$res = $this->comments;
$this->comments = [];
return $res;
}
/** @phpstan-impure */
public function tryConsumeTokenType(int $tokenType): bool
{
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) {
return false;
}
if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) {
if ($this->newline === null) {
$this->detectNewline();
}
}
$this->next();
return true;
}
/**
* @deprecated Use skipNewLineTokensAndConsumeComments instead (when parsing a type)
*/
public function skipNewLineTokens(): void
{
if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
return;
}
do {
$foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
} while ($foundNewLine === true);
}
public function skipNewLineTokensAndConsumeComments(): void
{
if ($this->currentTokenType() === Lexer::TOKEN_COMMENT) {
$this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex());
$this->next();
}
if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
return;
}
do {
$foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
if ($this->currentTokenType() !== Lexer::TOKEN_COMMENT) {
continue;
}
$this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex());
$this->next();
} while ($foundNewLine === true);
}
private function detectNewline(): void
{
$value = $this->currentTokenValue();
if (substr($value, 0, 2) === "\r\n") {
$this->newline = "\r\n";
} elseif (substr($value, 0, 1) === "\n") {
$this->newline = "\n";
}
}
public function getSkippedHorizontalWhiteSpaceIfAny(): string
{
if ($this->index > 0 && $this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
return $this->tokens[$this->index - 1][Lexer::VALUE_OFFSET];
}
return '';
}
/** @phpstan-impure */
public function joinUntil(int ...$tokenType): string
{
$s = '';
while (!in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true)) {
$s .= $this->tokens[$this->index++][Lexer::VALUE_OFFSET];
}
return $s;
}
public function next(): void
{
$this->index++;
$this->skipIrrelevantTokens();
}
private function skipIrrelevantTokens(): void
{
if (!isset($this->tokens[$this->index])) {
return;
}
while (in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) {
if (!isset($this->tokens[$this->index + 1])) {
break;
}
$this->index++;
}
}
public function addEndOfLineToSkippedTokens(): void
{
$this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS, Lexer::TOKEN_PHPDOC_EOL];
}
public function removeEndOfLineFromSkippedTokens(): void
{
$this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS];
}
/** @phpstan-impure */
public function forwardToTheEnd(): void
{
$lastToken = count($this->tokens) - 1;
$this->index = $lastToken;
}
public function pushSavePoint(): void
{
$this->savePoints[] = [$this->index, $this->comments];
}
public function dropSavePoint(): void
{
array_pop($this->savePoints);
}
public function rollback(): void
{
$savepoint = array_pop($this->savePoints);
assert($savepoint !== null);
[$this->index, $this->comments] = $savepoint;
}
/**
* @throws ParserException
*/
private function throwError(int $expectedTokenType, ?string $expectedTokenValue = null): void
{
throw new ParserException(
$this->currentTokenValue(),
$this->currentTokenType(),
$this->currentTokenOffset(),
$expectedTokenType,
$expectedTokenValue,
$this->currentTokenLine(),
);
}
/**
* Check whether the position is directly preceded by a certain token type.
*
* During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped
*/
public function hasTokenImmediatelyBefore(int $pos, int $expectedTokenType): bool
{
$tokens = $this->tokens;
$pos--;
for (; $pos >= 0; $pos--) {
$token = $tokens[$pos];
$type = $token[Lexer::TYPE_OFFSET];
if ($type === $expectedTokenType) {
return true;
}
if (!in_array($type, [
Lexer::TOKEN_HORIZONTAL_WS,
Lexer::TOKEN_PHPDOC_EOL,
], true)) {
break;
}
}
return false;
}
/**
* Check whether the position is directly followed by a certain token type.
*
* During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped
*/
public function hasTokenImmediatelyAfter(int $pos, int $expectedTokenType): bool
{
$tokens = $this->tokens;
$pos++;
for ($c = count($tokens); $pos < $c; $pos++) {
$token = $tokens[$pos];
$type = $token[Lexer::TYPE_OFFSET];
if ($type === $expectedTokenType) {
return true;
}
if (!in_array($type, [
Lexer::TOKEN_HORIZONTAL_WS,
Lexer::TOKEN_PHPDOC_EOL,
], true)) {
break;
}
}
return false;
}
public function getDetectedNewline(): ?string
{
return $this->newline;
}
/**
* Whether the given position is immediately surrounded by parenthesis.
*/
public function hasParentheses(int $startPos, int $endPos): bool
{
return $this->hasTokenImmediatelyBefore($startPos, Lexer::TOKEN_OPEN_PARENTHESES)
&& $this->hasTokenImmediatelyAfter($endPos, Lexer::TOKEN_CLOSE_PARENTHESES);
}
}

File diff suppressed because it is too large Load Diff