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,153 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\StringNotation;
use PhpCsFixer\AbstractProxyFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
/**
* @deprecated Use `string_implicit_backslashes` with config: ['single_quoted' => 'ignore', 'double_quoted' => 'escape', 'heredoc' => 'escape'] (default)
*
* @phpstan-type _AutogeneratedInputConfiguration array{
* double_quoted?: bool,
* heredoc_syntax?: bool,
* single_quoted?: bool,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* double_quoted: bool,
* heredoc_syntax: bool,
* single_quoted: bool,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Filippo Tessarotto <zoeslam@gmail.com>
* @author Michael Vorisek <https://github.com/mvorisek>
*/
final class EscapeImplicitBackslashesFixer extends AbstractProxyFixer implements ConfigurableFixerInterface, DeprecatedFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
public function getSuccessorsNames(): array
{
return array_keys($this->proxyFixers);
}
public function getDefinition(): FixerDefinitionInterface
{
$codeSample = <<<'EOF'
<?php
$singleQuoted = 'String with \" and My\Prefix\\';
$doubleQuoted = "Interpret my \n but not my \a";
$hereDoc = <<<HEREDOC
Interpret my \100 but not my \999
HEREDOC;
EOF;
return new FixerDefinition(
'Escape implicit backslashes in strings and heredocs to ease the understanding of which are special chars interpreted by PHP and which not.',
[
new CodeSample($codeSample),
new CodeSample(
$codeSample,
['single_quoted' => true]
),
new CodeSample(
$codeSample,
['double_quoted' => false]
),
new CodeSample(
$codeSample,
['heredoc_syntax' => false]
),
],
'In PHP double-quoted strings and heredocs some chars like `n`, `$` or `u` have special meanings if preceded by a backslash '
.'(and some are special only if followed by other special chars), while a backslash preceding other chars are interpreted like a plain '
.'backslash. The precise list of those special chars is hard to remember and to identify quickly: this fixer escapes backslashes '
."that do not start a special interpretation with the char after them.\n"
.'It is possible to fix also single-quoted strings: in this case there is no special chars apart from single-quote and backslash '
.'itself, so the fixer simply ensure that all backslashes are escaped. Both single and double backslashes are allowed in single-quoted '
.'strings, so the purpose in this context is mainly to have a uniformed way to have them written all over the codebase.'
);
}
/**
* {@inheritdoc}
*
* Must run before HeredocToNowdocFixer, SingleQuoteFixer.
* Must run after MultilineStringToHeredocFixer.
*/
public function getPriority(): int
{
return parent::getPriority();
}
protected function configurePostNormalisation(): void
{
/** @var StringImplicitBackslashesFixer */
$stringImplicitBackslashesFixer = $this->proxyFixers['string_implicit_backslashes'];
$stringImplicitBackslashesFixer->configure([
'single_quoted' => true === $this->configuration['single_quoted'] ? 'escape' : 'ignore',
'double_quoted' => true === $this->configuration['double_quoted'] ? 'escape' : 'ignore',
'heredoc' => true === $this->configuration['heredoc_syntax'] ? 'escape' : 'ignore',
]);
}
protected function createProxyFixers(): array
{
$stringImplicitBackslashesFixer = new StringImplicitBackslashesFixer();
$stringImplicitBackslashesFixer->configure([
'single_quoted' => 'ignore',
'double_quoted' => 'escape',
'heredoc' => 'escape',
]);
return [
$stringImplicitBackslashesFixer,
];
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('single_quoted', 'Whether to fix single-quoted strings.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
(new FixerOptionBuilder('double_quoted', 'Whether to fix double-quoted strings.'))
->setAllowedTypes(['bool'])
->setDefault(true)
->getOption(),
(new FixerOptionBuilder('heredoc_syntax', 'Whether to fix heredoc syntax.'))
->setAllowedTypes(['bool'])
->setDefault(true)
->getOption(),
]);
}
}

View File

@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\StringNotation;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class ExplicitStringVariableFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Converts implicit variables into explicit ones in double-quoted strings or heredoc syntax.',
[new CodeSample(
<<<'EOT'
<?php
$a = "My name is $name !";
$b = "I live in $state->country !";
$c = "I have $farm[0] chickens !";
EOT
)],
'The reasoning behind this rule is the following:'
."\n".'- When there are two valid ways of doing the same thing, using both is confusing, there should be a coding standard to follow.'
."\n".'- PHP manual marks `"$var"` syntax as implicit and `"{$var}"` syntax as explicit: explicit code should always be preferred.'
."\n".'- Explicit syntax allows word concatenation inside strings, e.g. `"{$var}IsAVar"`, implicit doesn\'t.'
."\n".'- Explicit syntax is easier to detect for IDE/editors and therefore has colors/highlight with higher contrast, which is easier to read.'
."\n".'Backtick operator is skipped because it is harder to handle; you can use `backtick_to_shell_exec` fixer to normalize backticks to strings.'
);
}
/**
* {@inheritdoc}
*
* Must run before NoUselessConcatOperatorFixer.
* Must run after BacktickToShellExecFixer.
*/
public function getPriority(): int
{
return 6;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_VARIABLE);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$backtickStarted = false;
for ($index = \count($tokens) - 1; $index > 0; --$index) {
$token = $tokens[$index];
if ($token->equals('`')) {
$backtickStarted = !$backtickStarted;
continue;
}
if ($backtickStarted || !$token->isGivenKind(\T_VARIABLE)) {
continue;
}
$prevToken = $tokens[$index - 1];
if (!$this->isStringPartToken($prevToken)) {
continue;
}
$distinctVariableIndex = $index;
$variableTokens = [
$distinctVariableIndex => [
'tokens' => [$index => $token],
'firstVariableTokenIndex' => $index,
'lastVariableTokenIndex' => $index,
],
];
$nextIndex = $index + 1;
$squareBracketCount = 0;
while (!$this->isStringPartToken($tokens[$nextIndex])) {
if ($tokens[$nextIndex]->isGivenKind(\T_CURLY_OPEN)) {
$nextIndex = $tokens->getNextTokenOfKind($nextIndex, [[CT::T_CURLY_CLOSE]]);
} elseif ($tokens[$nextIndex]->isGivenKind(\T_VARIABLE) && 1 !== $squareBracketCount) {
$distinctVariableIndex = $nextIndex;
$variableTokens[$distinctVariableIndex] = [
'tokens' => [$nextIndex => $tokens[$nextIndex]],
'firstVariableTokenIndex' => $nextIndex,
'lastVariableTokenIndex' => $nextIndex,
];
} else {
$variableTokens[$distinctVariableIndex]['tokens'][$nextIndex] = $tokens[$nextIndex];
$variableTokens[$distinctVariableIndex]['lastVariableTokenIndex'] = $nextIndex;
if ($tokens[$nextIndex]->equalsAny(['[', ']'])) {
++$squareBracketCount;
}
}
++$nextIndex;
}
krsort($variableTokens, \SORT_NUMERIC);
foreach ($variableTokens as $distinctVariableSet) {
if (1 === \count($distinctVariableSet['tokens'])) {
$singleVariableIndex = array_key_first($distinctVariableSet['tokens']);
$singleVariableToken = current($distinctVariableSet['tokens']);
$tokens->overrideRange($singleVariableIndex, $singleVariableIndex, [
new Token([\T_CURLY_OPEN, '{']),
new Token([\T_VARIABLE, $singleVariableToken->getContent()]),
new Token([CT::T_CURLY_CLOSE, '}']),
]);
} else {
foreach ($distinctVariableSet['tokens'] as $variablePartIndex => $variablePartToken) {
if ($variablePartToken->isGivenKind(\T_NUM_STRING)) {
$tokens[$variablePartIndex] = new Token([\T_LNUMBER, $variablePartToken->getContent()]);
continue;
}
if ($variablePartToken->isGivenKind(\T_STRING) && $tokens[$variablePartIndex + 1]->equals(']')) {
$tokens[$variablePartIndex] = new Token([\T_CONSTANT_ENCAPSED_STRING, "'".$variablePartToken->getContent()."'"]);
}
}
$tokens->insertAt($distinctVariableSet['lastVariableTokenIndex'] + 1, new Token([CT::T_CURLY_CLOSE, '}']));
$tokens->insertAt($distinctVariableSet['firstVariableTokenIndex'], new Token([\T_CURLY_OPEN, '{']));
}
}
}
}
/**
* Check if token is a part of a string.
*
* @param Token $token The token to check
*/
private function isStringPartToken(Token $token): bool
{
return $token->isGivenKind(\T_ENCAPSED_AND_WHITESPACE)
|| $token->isGivenKind(\T_START_HEREDOC)
|| '"' === $token->getContent()
|| 'b"' === strtolower($token->getContent());
}
}

View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\StringNotation;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* closing_marker?: string,
* explicit_heredoc_style?: bool,
* reserved_closing_markers?: list<string>,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* closing_marker: string,
* explicit_heredoc_style: bool,
* reserved_closing_markers: list<string>,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Michael Vorisek <https://github.com/mvorisek>
*/
final class HeredocClosingMarkerFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
/**
* @var list<string>
*/
public const RESERVED_CLOSING_MARKERS = [
'CSS',
'DIFF',
'HTML',
'JS',
'JSON',
'MD',
'PHP',
'PYTHON',
'RST',
'TS',
'SQL',
'XML',
'YAML',
];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Unify `heredoc` or `nowdoc` closing marker.',
[
new CodeSample(
<<<'EOD'
<?php $a = <<<"TEST"
Foo
TEST;
EOD
),
new CodeSample(
<<<'EOD'
<?php $a = <<<'TEST'
Foo
TEST;
EOD,
['closing_marker' => 'EOF']
),
new CodeSample(
<<<'EOD_'
<?php $a = <<<EOD
Foo
EOD;
EOD_,
['explicit_heredoc_style' => true]
),
]
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_START_HEREDOC);
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder(
'closing_marker',
'Preferred closing marker.'
))
->setAllowedTypes(['string'])
->setDefault('EOD')
->getOption(),
(new FixerOptionBuilder(
'reserved_closing_markers',
'Reserved closing markers to be kept unchanged.'
))
->setAllowedTypes(['string[]'])
->setDefault(self::RESERVED_CLOSING_MARKERS)
->getOption(),
(new FixerOptionBuilder(
'explicit_heredoc_style',
'Whether the closing marker should be wrapped in double quotes.'
))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$reservedClosingMarkersMap = null;
$startIndex = null;
foreach ($tokens as $index => $token) {
if ($token->isGivenKind(\T_START_HEREDOC)) {
$startIndex = $index;
continue;
}
if (null !== $startIndex && $token->isGivenKind(\T_END_HEREDOC)) {
$existingClosingMarker = trim($token->getContent());
if (null === $reservedClosingMarkersMap) {
$reservedClosingMarkersMap = [];
foreach ($this->configuration['reserved_closing_markers'] as $v) {
$reservedClosingMarkersMap[mb_strtoupper($v)] = $v;
}
}
$existingClosingMarker = mb_strtoupper($existingClosingMarker);
do {
$newClosingMarker = $reservedClosingMarkersMap[$existingClosingMarker] ?? null;
if (!str_ends_with($existingClosingMarker, '_')) {
break;
}
$existingClosingMarker = substr($existingClosingMarker, 0, -1);
} while (null === $newClosingMarker);
if (null === $newClosingMarker) {
$newClosingMarker = $this->configuration['closing_marker'];
}
$content = $tokens->generatePartialCode($startIndex + 1, $index - 1);
while (Preg::match('~(^|[\r\n])\s*'.preg_quote($newClosingMarker, '~').'(?!\w)~', $content)) {
$newClosingMarker .= '_';
}
[$tokens[$startIndex], $tokens[$index]] = $this->convertClosingMarker($tokens[$startIndex], $token, $newClosingMarker);
$startIndex = null;
continue;
}
}
}
/**
* @return array{Token, Token}
*/
private function convertClosingMarker(Token $startToken, Token $endToken, string $newClosingMarker): array
{
$isNowdoc = str_contains($startToken->getContent(), '\'');
$markerQuote = $isNowdoc
? '\''
: (true === $this->configuration['explicit_heredoc_style'] ? '"' : '');
return [new Token([
$startToken->getId(),
Preg::replace('/<<<\h*\K["\']?[^\s"\']+["\']?/', $markerQuote.$newClosingMarker.$markerQuote, $startToken->getContent()),
]), new Token([
$endToken->getId(),
Preg::replace('/\S+/', $newClosingMarker, $endToken->getContent()),
])];
}
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\StringNotation;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Gregor Harlan <gharlan@web.de>
*/
final class HeredocToNowdocFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Convert `heredoc` to `nowdoc` where possible.',
[
new CodeSample(
<<<'EOF'
<?php $a = <<<"TEST"
Foo
TEST;
EOF
),
]
);
}
/**
* {@inheritdoc}
*
* Must run after EscapeImplicitBackslashesFixer, StringImplicitBackslashesFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_START_HEREDOC);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(\T_START_HEREDOC) || str_contains($token->getContent(), "'")) {
continue;
}
if ($tokens[$index + 1]->isGivenKind(\T_END_HEREDOC)) {
$tokens[$index] = $this->convertToNowdoc($token);
continue;
}
if (
!$tokens[$index + 1]->isGivenKind(\T_ENCAPSED_AND_WHITESPACE)
|| !$tokens[$index + 2]->isGivenKind(\T_END_HEREDOC)
) {
continue;
}
$content = $tokens[$index + 1]->getContent();
// regex: odd number of backslashes, not followed by dollar
if (Preg::match('/(?<!\\\)(?:\\\{2})*\\\(?![$\\\])/', $content)) {
continue;
}
$tokens[$index] = $this->convertToNowdoc($token);
$content = str_replace(['\\\\', '\$'], ['\\', '$'], $content);
$tokens[$index + 1] = new Token([
$tokens[$index + 1]->getId(),
$content,
]);
}
}
/**
* Transforms the heredoc start token to nowdoc notation.
*/
private function convertToNowdoc(Token $token): Token
{
return new Token([
$token->getId(),
Preg::replace('/^([Bb]?<<<)(\h*)"?([^\s"]+)"?/', '$1$2\'$3\'', $token->getContent()),
]);
}
}

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\StringNotation;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Michael Vorisek <https://github.com/mvorisek>
*/
final class MultilineStringToHeredocFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Convert multiline string to `heredoc` or `nowdoc`.',
[
new CodeSample(
<<<'EOD'
<?php
$a = 'line1
line2';
EOD."\n"
),
new CodeSample(
<<<'EOD'
<?php
$a = "line1
{$obj->getName()}";
EOD."\n"
),
]
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_CONSTANT_ENCAPSED_STRING, \T_ENCAPSED_AND_WHITESPACE]);
}
/**
* {@inheritdoc}
*
* Must run before EscapeImplicitBackslashesFixer, HeredocIndentationFixer, StringImplicitBackslashesFixer.
*/
public function getPriority(): int
{
return 16;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$complexStringStartIndex = null;
foreach ($tokens as $index => $token) {
if (null === $complexStringStartIndex) {
if ($token->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
$this->convertStringToHeredoc($tokens, $index, $index);
} elseif ($token->equalsAny(['"', 'b"', 'B"'])) {
$complexStringStartIndex = $index;
}
} elseif ($token->equals('"')) {
$this->convertStringToHeredoc($tokens, $complexStringStartIndex, $index);
$complexStringStartIndex = null;
}
}
}
private function convertStringToHeredoc(Tokens $tokens, int $stringStartIndex, int $stringEndIndex): void
{
$closingMarker = 'EOD';
if ($tokens[$stringStartIndex]->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
$content = $tokens[$stringStartIndex]->getContent();
if ('b' === strtolower(substr($content, 0, 1))) {
$content = substr($content, 1);
}
$isSingleQuoted = str_starts_with($content, '\'');
$content = substr($content, 1, -1);
if ($isSingleQuoted) {
$content = Preg::replace('~\\\([\\\\\'])~', '$1', $content);
} else {
$content = Preg::replace('~(\\\\\\\)|\\\(")~', '$1$2', $content);
}
$constantStringToken = new Token([\T_ENCAPSED_AND_WHITESPACE, $content."\n"]);
} else {
$content = $tokens->generatePartialCode($stringStartIndex + 1, $stringEndIndex - 1);
$isSingleQuoted = false;
$constantStringToken = null;
}
if (!str_contains($content, "\n") && !str_contains($content, "\r")) {
return;
}
while (Preg::match('~(^|[\r\n])\s*'.preg_quote($closingMarker, '~').'(?!\w)~', $content)) {
$closingMarker .= '_';
}
$quoting = $isSingleQuoted ? '\'' : '';
$heredocStartToken = new Token([\T_START_HEREDOC, '<<<'.$quoting.$closingMarker.$quoting."\n"]);
$heredocEndToken = new Token([\T_END_HEREDOC, $closingMarker]);
if (null !== $constantStringToken) {
$tokens->overrideRange($stringStartIndex, $stringEndIndex, [
$heredocStartToken,
$constantStringToken,
$heredocEndToken,
]);
} else {
for ($i = $stringStartIndex + 1; $i < $stringEndIndex; ++$i) {
if ($tokens[$i]->isGivenKind(\T_ENCAPSED_AND_WHITESPACE)) {
$tokens[$i] = new Token([
$tokens[$i]->getId(),
Preg::replace('~(\\\\\\\)|\\\(")~', '$1$2', $tokens[$i]->getContent()),
]);
}
}
$tokens[$stringStartIndex] = $heredocStartToken;
$tokens[$stringEndIndex] = $heredocEndToken;
if ($tokens[$stringEndIndex - 1]->isGivenKind(\T_ENCAPSED_AND_WHITESPACE)) {
$tokens[$stringEndIndex - 1] = new Token([
$tokens[$stringEndIndex - 1]->getId(),
$tokens[$stringEndIndex - 1]->getContent()."\n",
]);
} else {
$tokens->insertAt($stringEndIndex, new Token([
\T_ENCAPSED_AND_WHITESPACE,
"\n",
]));
}
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\StringNotation;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author ntzm
*/
final class NoBinaryStringFixer extends AbstractFixer
{
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound(
[
\T_CONSTANT_ENCAPSED_STRING,
\T_START_HEREDOC,
'b"',
]
);
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'There should not be a binary flag before strings.',
[
new CodeSample("<?php \$a = b'foo';\n"),
new CodeSample("<?php \$a = b<<<EOT\nfoo\nEOT;\n"),
]
);
}
/**
* {@inheritdoc}
*
* Must run before NoUselessConcatOperatorFixer, PhpUnitDedicateAssertInternalTypeFixer, RegularCallableCallFixer, SetTypeToCastFixer.
*/
public function getPriority(): int
{
return 40;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if ($token->isGivenKind([\T_CONSTANT_ENCAPSED_STRING, \T_START_HEREDOC])) {
$content = $token->getContent();
if ('b' === strtolower($content[0])) {
$tokens[$index] = new Token([$token->getId(), substr($content, 1)]);
}
} elseif ($token->equals('b"')) {
$tokens[$index] = new Token('"');
}
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\StringNotation;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Gregor Harlan
*/
final class NoTrailingWhitespaceInStringFixer extends AbstractFixer
{
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_CONSTANT_ENCAPSED_STRING, \T_ENCAPSED_AND_WHITESPACE, \T_INLINE_HTML]);
}
public function isRisky(): bool
{
return true;
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'There must be no trailing whitespace in strings.',
[
new CodeSample(
"<?php \$a = ' \n foo \n';\n"
),
],
null,
'Changing the whitespaces in strings might affect string comparisons and outputs.'
);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = $tokens->count() - 1, $last = true; $index >= 0; --$index, $last = false) {
$token = $tokens[$index];
if (!$token->isGivenKind([\T_CONSTANT_ENCAPSED_STRING, \T_ENCAPSED_AND_WHITESPACE, \T_INLINE_HTML])) {
continue;
}
$isInlineHtml = $token->isGivenKind(\T_INLINE_HTML);
$regex = $isInlineHtml && $last ? '/\h+(?=\R|$)/' : '/\h+(?=\R)/';
$content = Preg::replace($regex, '', $token->getContent());
if ($token->getContent() === $content) {
continue;
}
if (!$isInlineHtml || 0 === $index) {
$this->updateContent($tokens, $index, $content);
continue;
}
$prev = $index - 1;
if ($tokens[$prev]->equals([\T_CLOSE_TAG, '?>']) && Preg::match('/^\R/', $content, $match)) {
$tokens[$prev] = new Token([\T_CLOSE_TAG, $tokens[$prev]->getContent().$match[0]]);
$content = substr($content, \strlen($match[0]));
$content = false === $content ? '' : $content; // @phpstan-ignore-line due to https://github.com/phpstan/phpstan/issues/1215 , awaiting PHP8 as min requirement of Fixer
}
$this->updateContent($tokens, $index, $content);
}
}
private function updateContent(Tokens $tokens, int $index, string $content): void
{
if ('' === $content) {
$tokens->clearAt($index);
return;
}
$tokens[$index] = new Token([$tokens[$index]->getId(), $content]);
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\StringNotation;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dave van der Brugge <dmvdbrugge@gmail.com>
*/
final class SimpleToComplexStringVariableFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Converts explicit variables in double-quoted strings and heredoc syntax from simple to complex format (`${` to `{$`).',
[
new CodeSample(
<<<'EOT'
<?php
$name = 'World';
echo "Hello ${name}!";
EOT
),
new CodeSample(
<<<'EOT'
<?php
$name = 'World';
echo <<<TEST
Hello ${name}!
TEST;
EOT
),
],
"Doesn't touch implicit variables. Works together nicely with `explicit_string_variable`."
);
}
/**
* {@inheritdoc}
*
* Must run after ExplicitStringVariableFixer.
*/
public function getPriority(): int
{
return -10;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_DOLLAR_OPEN_CURLY_BRACES);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = \count($tokens) - 3; $index > 0; --$index) {
if (!$tokens[$index]->isGivenKind(\T_DOLLAR_OPEN_CURLY_BRACES)) {
continue;
}
$varnameToken = $tokens[$index + 1];
if (!$varnameToken->isGivenKind(\T_STRING_VARNAME)) {
continue;
}
$dollarCloseToken = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_COMPLEX_STRING_VARIABLE, $index);
$prevTokenContent = $tokens[$index - 1]->getContent();
if (str_ends_with($prevTokenContent, '$') && !str_ends_with($prevTokenContent, '\$')) {
$tokens[$index - 1] = new Token([\T_ENCAPSED_AND_WHITESPACE, substr($prevTokenContent, 0, -1).'\$']);
}
$tokens[$index] = new Token([\T_CURLY_OPEN, '{']);
$tokens[$index + 1] = new Token([\T_VARIABLE, '$'.$varnameToken->getContent()]);
$tokens[$dollarCloseToken] = new Token([CT::T_CURLY_CLOSE, '}']);
}
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\StringNotation;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* strings_containing_single_quote_chars?: bool,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* strings_containing_single_quote_chars: bool,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Gregor Harlan <gharlan@web.de>
*/
final class SingleQuoteFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
public function getDefinition(): FixerDefinitionInterface
{
$codeSample = <<<'EOF'
<?php
$a = "sample";
$b = "sample with 'single-quotes'";
EOF;
return new FixerDefinition(
'Convert double quotes to single quotes for simple strings.',
[
new CodeSample($codeSample),
new CodeSample(
$codeSample,
['strings_containing_single_quote_chars' => true]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before NoUselessConcatOperatorFixer.
* Must run after BacktickToShellExecFixer, EscapeImplicitBackslashesFixer, StringImplicitBackslashesFixer.
*/
public function getPriority(): int
{
return 10;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_CONSTANT_ENCAPSED_STRING);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
continue;
}
$content = $token->getContent();
$prefix = '';
if ('b' === strtolower($content[0])) {
$prefix = $content[0];
$content = substr($content, 1);
}
if (
'"' === $content[0]
&& (true === $this->configuration['strings_containing_single_quote_chars'] || !str_contains($content, "'"))
// regex: odd number of backslashes, not followed by double quote or dollar
&& !Preg::match('/(?<!\\\)(?:\\\{2})*\\\(?!["$\\\])/', $content)
) {
$content = substr($content, 1, -1);
$content = str_replace(['\"', '\$', '\''], ['"', '$', '\\\''], $content);
$tokens[$index] = new Token([\T_CONSTANT_ENCAPSED_STRING, $prefix.'\''.$content.'\'']);
}
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('strings_containing_single_quote_chars', 'Whether to fix double-quoted strings that contains single-quotes.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
}

View File

@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\StringNotation;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* double_quoted?: 'escape'|'ignore'|'unescape',
* heredoc?: 'escape'|'ignore'|'unescape',
* single_quoted?: 'escape'|'ignore'|'unescape',
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* double_quoted: 'escape'|'ignore'|'unescape',
* heredoc: 'escape'|'ignore'|'unescape',
* single_quoted: 'escape'|'ignore'|'unescape',
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Filippo Tessarotto <zoeslam@gmail.com>
* @author Michael Vorisek <https://github.com/mvorisek>
*/
final class StringImplicitBackslashesFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
public function getDefinition(): FixerDefinitionInterface
{
$codeSample = <<<'EOF'
<?php
$singleQuoted = 'String with \" and My\Prefix\\';
$doubleQuoted = "Interpret my \n but not my \a";
$hereDoc = <<<HEREDOC
Interpret my \100 but not my \999
HEREDOC;
EOF;
return new FixerDefinition(
'Handles implicit backslashes in strings and heredocs. Depending on the chosen strategy, it can escape implicit backslashes to ease the understanding of which are special chars interpreted by PHP and which not (`escape`), or it can remove these additional backslashes if you find them superfluous (`unescape`). You can also leave them as-is using `ignore` strategy.',
[
new CodeSample($codeSample),
new CodeSample(
$codeSample,
['single_quoted' => 'escape']
),
new CodeSample(
$codeSample,
['double_quoted' => 'unescape']
),
new CodeSample(
$codeSample,
['heredoc' => 'unescape']
),
],
'In PHP double-quoted strings and heredocs some chars like `n`, `$` or `u` have special meanings if preceded by a backslash '
.'(and some are special only if followed by other special chars), while a backslash preceding other chars are interpreted like a plain '
.'backslash. The precise list of those special chars is hard to remember and to identify quickly: this fixer escapes backslashes '
."that do not start a special interpretation with the char after them.\n"
.'It is possible to fix also single-quoted strings: in this case there is no special chars apart from single-quote and backslash '
.'itself, so the fixer simply ensure that all backslashes are escaped. Both single and double backslashes are allowed in single-quoted '
.'strings, so the purpose in this context is mainly to have a uniformed way to have them written all over the codebase.'
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_ENCAPSED_AND_WHITESPACE, \T_CONSTANT_ENCAPSED_STRING]);
}
/**
* {@inheritdoc}
*
* Must run before HeredocToNowdocFixer, SingleQuoteFixer.
* Must run after MultilineStringToHeredocFixer.
*/
public function getPriority(): int
{
return 15;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$singleQuotedReservedRegex = '[\'\\\]';
$doubleQuotedReservedRegex = '(?:[efnrtv$"\\\0-7]|x[0-9A-Fa-f]|u{|$)';
$heredocSyntaxReservedRegex = '(?:[efnrtv$\\\0-7]|x[0-9A-Fa-f]|u{|$)';
$doubleQuoteOpened = false;
foreach ($tokens as $index => $token) {
if ($token->equalsAny(['"', 'b"', 'B"'])) {
$doubleQuoteOpened = !$doubleQuoteOpened;
}
if (!$token->isGivenKind([\T_ENCAPSED_AND_WHITESPACE, \T_CONSTANT_ENCAPSED_STRING])) {
continue;
}
$content = $token->getContent();
if (!str_contains($content, '\\')) {
continue;
}
// nowdoc syntax
if ($token->isGivenKind(\T_ENCAPSED_AND_WHITESPACE) && '\'' === substr(rtrim($tokens[$index - 1]->getContent()), -1)) {
continue;
}
$firstTwoCharacters = strtolower(substr($content, 0, 2));
$isSingleQuotedString = $token->isGivenKind(\T_CONSTANT_ENCAPSED_STRING) && ('\'' === $content[0] || 'b\'' === $firstTwoCharacters);
$isDoubleQuotedString = ($token->isGivenKind(\T_CONSTANT_ENCAPSED_STRING) && ('"' === $content[0] || 'b"' === $firstTwoCharacters))
|| ($token->isGivenKind(\T_ENCAPSED_AND_WHITESPACE) && $doubleQuoteOpened);
if ($isSingleQuotedString
? 'ignore' === $this->configuration['single_quoted']
: ($isDoubleQuotedString
? 'ignore' === $this->configuration['double_quoted']
: 'ignore' === $this->configuration['heredoc'])
) {
continue;
}
$escapeBackslashes = $isSingleQuotedString
? 'escape' === $this->configuration['single_quoted']
: ($isDoubleQuotedString
? 'escape' === $this->configuration['double_quoted']
: 'escape' === $this->configuration['heredoc']);
$reservedRegex = $isSingleQuotedString
? $singleQuotedReservedRegex
: ($isDoubleQuotedString
? $doubleQuotedReservedRegex
: $heredocSyntaxReservedRegex);
if ($escapeBackslashes) {
$regex = '/(?<!\\\)\\\((?:\\\\\\\)*)(?!'.$reservedRegex.')/';
$newContent = Preg::replace($regex, '\\\\\\\$1', $content);
} else {
$regex = '/(?<!\\\)\\\\\\\((?:\\\\\\\)*)(?!'.$reservedRegex.')/';
$newContent = Preg::replace($regex, '\\\$1', $content);
}
if ($newContent !== $content) {
$tokens[$index] = new Token([$token->getId(), $newContent]);
}
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('single_quoted', 'Whether to escape backslashes in single-quoted strings.'))
->setAllowedValues(['escape', 'unescape', 'ignore'])
->setDefault('unescape')
->getOption(),
(new FixerOptionBuilder('double_quoted', 'Whether to escape backslashes in double-quoted strings.'))
->setAllowedValues(['escape', 'unescape', 'ignore'])
->setDefault('escape')
->getOption(),
(new FixerOptionBuilder('heredoc', 'Whether to escape backslashes in heredoc syntax.'))
->setAllowedValues(['escape', 'unescape', 'ignore'])
->setDefault('escape')
->getOption(),
]);
}
}

View File

@@ -0,0 +1,320 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\StringNotation;
use PhpCsFixer\AbstractFunctionReferenceFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
final class StringLengthToEmptyFixer extends AbstractFunctionReferenceFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'String tests for empty must be done against `\'\'`, not with `strlen`.',
[new CodeSample("<?php \$a = 0 === strlen(\$b) || \\STRLEN(\$c) < 1;\n")],
null,
'Risky when `strlen` is overridden, when called using a `stringable` object, also no longer triggers warning when called using non-string(able).'
);
}
/**
* {@inheritdoc}
*
* Must run before NoExtraBlankLinesFixer, NoTrailingWhitespaceFixer.
* Must run after NoSpacesInsideParenthesisFixer, SpacesInsideParenthesesFixer.
*/
public function getPriority(): int
{
return 1;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$argumentsAnalyzer = new ArgumentsAnalyzer();
foreach ($this->findStrLengthCalls($tokens) as $candidate) {
[$functionNameIndex, $openParenthesisIndex, $closeParenthesisIndex] = $candidate;
$arguments = $argumentsAnalyzer->getArguments($tokens, $openParenthesisIndex, $closeParenthesisIndex);
if (1 !== \count($arguments)) {
continue; // must be one argument
}
// test for leading `\` before `strlen` call
$nextIndex = $tokens->getNextMeaningfulToken($closeParenthesisIndex);
$previousIndex = $tokens->getPrevMeaningfulToken($functionNameIndex);
if ($tokens[$previousIndex]->isGivenKind(\T_NS_SEPARATOR)) {
$namespaceSeparatorIndex = $previousIndex;
$previousIndex = $tokens->getPrevMeaningfulToken($previousIndex);
} else {
$namespaceSeparatorIndex = null;
}
// test for yoda vs non-yoda fix case
if ($this->isOperatorOfInterest($tokens[$previousIndex])) { // test if valid yoda case to fix
$operatorIndex = $previousIndex;
$operandIndex = $tokens->getPrevMeaningfulToken($previousIndex);
if (!$this->isOperandOfInterest($tokens[$operandIndex])) { // test if operand is `0` or `1`
continue;
}
$replacement = $this->getReplacementYoda($tokens[$operatorIndex], $tokens[$operandIndex]);
if (null === $replacement) {
continue;
}
if ($this->isOfHigherPrecedence($tokens[$nextIndex])) { // is of higher precedence right; continue
continue;
}
if ($this->isOfHigherPrecedence($tokens[$tokens->getPrevMeaningfulToken($operandIndex)])) { // is of higher precedence left; continue
continue;
}
} elseif ($this->isOperatorOfInterest($tokens[$nextIndex])) { // test if valid !yoda case to fix
$operatorIndex = $nextIndex;
$operandIndex = $tokens->getNextMeaningfulToken($nextIndex);
if (!$this->isOperandOfInterest($tokens[$operandIndex])) { // test if operand is `0` or `1`
continue;
}
$replacement = $this->getReplacementNotYoda($tokens[$operatorIndex], $tokens[$operandIndex]);
if (null === $replacement) {
continue;
}
if ($this->isOfHigherPrecedence($tokens[$tokens->getNextMeaningfulToken($operandIndex)])) { // is of higher precedence right; continue
continue;
}
if ($this->isOfHigherPrecedence($tokens[$previousIndex])) { // is of higher precedence left; continue
continue;
}
} else {
continue;
}
// prepare for fixing
$keepParentheses = $this->keepParentheses($tokens, $openParenthesisIndex, $closeParenthesisIndex);
if (\T_IS_IDENTICAL === $replacement) {
$operandContent = '===';
} else { // T_IS_NOT_IDENTICAL === $replacement
$operandContent = '!==';
}
// apply fixing
$tokens[$operandIndex] = new Token([\T_CONSTANT_ENCAPSED_STRING, "''"]);
$tokens[$operatorIndex] = new Token([$replacement, $operandContent]);
if (!$keepParentheses) {
$tokens->clearTokenAndMergeSurroundingWhitespace($closeParenthesisIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($openParenthesisIndex);
}
$tokens->clearTokenAndMergeSurroundingWhitespace($functionNameIndex);
if (null !== $namespaceSeparatorIndex) {
$tokens->clearTokenAndMergeSurroundingWhitespace($namespaceSeparatorIndex);
}
}
}
private function getReplacementYoda(Token $operator, Token $operand): ?int
{
/* Yoda 0
0 === strlen($b) | '' === $b
0 !== strlen($b) | '' !== $b
0 <= strlen($b) | X makes no sense, assume overridden
0 >= strlen($b) | '' === $b
0 < strlen($b) | '' !== $b
0 > strlen($b) | X makes no sense, assume overridden
*/
if ('0' === $operand->getContent()) {
if ($operator->isGivenKind([\T_IS_IDENTICAL, \T_IS_GREATER_OR_EQUAL])) {
return \T_IS_IDENTICAL;
}
if ($operator->isGivenKind(\T_IS_NOT_IDENTICAL) || $operator->equals('<')) {
return \T_IS_NOT_IDENTICAL;
}
return null;
}
/* Yoda 1
1 === strlen($b) | X cannot simplify
1 !== strlen($b) | X cannot simplify
1 <= strlen($b) | '' !== $b
1 >= strlen($b) | cannot simplify
1 < strlen($b) | cannot simplify
1 > strlen($b) | '' === $b
*/
if ($operator->isGivenKind(\T_IS_SMALLER_OR_EQUAL)) {
return \T_IS_NOT_IDENTICAL;
}
if ($operator->equals('>')) {
return \T_IS_IDENTICAL;
}
return null;
}
private function getReplacementNotYoda(Token $operator, Token $operand): ?int
{
/* Not Yoda 0
strlen($b) === 0 | $b === ''
strlen($b) !== 0 | $b !== ''
strlen($b) <= 0 | $b === ''
strlen($b) >= 0 | X makes no sense, assume overridden
strlen($b) < 0 | X makes no sense, assume overridden
strlen($b) > 0 | $b !== ''
*/
if ('0' === $operand->getContent()) {
if ($operator->isGivenKind([\T_IS_IDENTICAL, \T_IS_SMALLER_OR_EQUAL])) {
return \T_IS_IDENTICAL;
}
if ($operator->isGivenKind(\T_IS_NOT_IDENTICAL) || $operator->equals('>')) {
return \T_IS_NOT_IDENTICAL;
}
return null;
}
/* Not Yoda 1
strlen($b) === 1 | X cannot simplify
strlen($b) !== 1 | X cannot simplify
strlen($b) <= 1 | X cannot simplify
strlen($b) >= 1 | $b !== ''
strlen($b) < 1 | $b === ''
strlen($b) > 1 | X cannot simplify
*/
if ($operator->isGivenKind(\T_IS_GREATER_OR_EQUAL)) {
return \T_IS_NOT_IDENTICAL;
}
if ($operator->equals('<')) {
return \T_IS_IDENTICAL;
}
return null;
}
private function isOperandOfInterest(Token $token): bool
{
if (!$token->isGivenKind(\T_LNUMBER)) {
return false;
}
$content = $token->getContent();
return '0' === $content || '1' === $content;
}
private function isOperatorOfInterest(Token $token): bool
{
return
$token->isGivenKind([\T_IS_IDENTICAL, \T_IS_NOT_IDENTICAL, \T_IS_SMALLER_OR_EQUAL, \T_IS_GREATER_OR_EQUAL])
|| $token->equals('<') || $token->equals('>');
}
private function isOfHigherPrecedence(Token $token): bool
{
return $token->isGivenKind([\T_INSTANCEOF, \T_POW, \T_SL, \T_SR]) || $token->equalsAny([
'!',
'%',
'*',
'+',
'-',
'.',
'/',
'~',
'?',
]);
}
private function keepParentheses(Tokens $tokens, int $openParenthesisIndex, int $closeParenthesisIndex): bool
{
$i = $tokens->getNextMeaningfulToken($openParenthesisIndex);
if ($tokens[$i]->isCast()) {
$i = $tokens->getNextMeaningfulToken($i);
}
for (; $i < $closeParenthesisIndex; ++$i) {
$token = $tokens[$i];
if ($token->isGivenKind([\T_VARIABLE, \T_STRING]) || $token->isObjectOperator() || $token->isWhitespace() || $token->isComment()) {
continue;
}
$blockType = Tokens::detectBlockType($token);
if (null !== $blockType && $blockType['isStart']) {
$i = $tokens->findBlockEnd($blockType['type'], $i);
continue;
}
return true;
}
return false;
}
private function findStrLengthCalls(Tokens $tokens): \Generator
{
$candidates = [];
$count = \count($tokens);
for ($i = 0; $i < $count; ++$i) {
$candidate = $this->find('strlen', $tokens, $i, $count);
if (null === $candidate) {
break;
}
$i = $candidate[1]; // proceed to openParenthesisIndex
$candidates[] = $candidate;
}
foreach (array_reverse($candidates) as $candidate) {
yield $candidate;
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\StringNotation;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Fixes the line endings in multi-line strings.
*
* @author Ilija Tovilo <ilija.tovilo@me.com>
*/
final class StringLineEndingFixer extends AbstractFixer implements WhitespacesAwareFixerInterface
{
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_CONSTANT_ENCAPSED_STRING, \T_ENCAPSED_AND_WHITESPACE, \T_INLINE_HTML]);
}
public function isRisky(): bool
{
return true;
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'All multi-line strings must use correct line ending.',
[
new CodeSample(
"<?php \$a = 'my\r\nmulti\nline\r\nstring';\r\n"
),
],
null,
'Changing the line endings of multi-line strings might affect string comparisons and outputs.'
);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$ending = $this->whitespacesConfig->getLineEnding();
foreach ($tokens as $tokenIndex => $token) {
if (!$token->isGivenKind([\T_CONSTANT_ENCAPSED_STRING, \T_ENCAPSED_AND_WHITESPACE, \T_INLINE_HTML])) {
continue;
}
$tokens[$tokenIndex] = new Token([
$token->getId(),
Preg::replace(
'#\R#u',
$ending,
$token->getContent()
),
]);
}
}
}