Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ parameters:
- 'Rector\CodeQuality\Rector\If_\SimplifyIfReturnBoolRector'
```

Do you want to skip just specific line with specific rule?
### How to Skip Specific Line + Specific Rule?

Use `@noRector \FQN name` annotation:

Expand Down
127 changes: 98 additions & 29 deletions packages/better-php-doc-parser/src/PhpDocNode/AbstractTagValueNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ abstract class AbstractTagValueNode implements AttributeAwareNodeInterface, PhpD
*/
protected $hasClosingBracket = false;

/**
* @var bool[]
*/
private $keysByQuotedStatus = [];

/**
* @var bool
*/
private $isSilentKeyExplicit = true;

/**
* @var string|null
*/
private $silentKey;

/**
* @param mixed[] $item
*/
Expand All @@ -64,17 +79,15 @@ protected function printArrayItem(array $item, ?string $key = null): string
// cleanup json encoded extra slashes
$json = Strings::replace($json, '#\\\\\\\\#', '\\');

if ($key) {
return sprintf('%s=%s', $key, $json);
}
$keyPart = $this->createKeyPart($key);

return $json;
}
// should unqote
if ($this->isValueWithoutQuotes($key)) {
// @todo resolve per key item
$json = Strings::replace($json, '#"#');
}

protected function printArrayItemWithoutQuotes(array $item, ?string $key = null): string
{
$content = $this->printArrayItem($item, $key);
return Strings::replace($content, '#"#');
return $keyPart . $json;
}

/**
Expand Down Expand Up @@ -118,9 +131,9 @@ protected function printContentItems(array $contentItems): string
protected function printNestedTag(
array $tagValueNodes,
string $label,
bool $haveFinalComma = false,
?string $openingSpace = null,
?string $closingSpace = null
bool $haveFinalComma,
?string $openingSpace,
?string $closingSpace
): string {
$tagValueNodesAsString = $this->printTagValueNodesSeparatedByComma($tagValueNodes);

Expand All @@ -144,6 +157,8 @@ protected function printNestedTag(

protected function resolveOriginalContentSpacingAndOrder(?string $originalContent, ?string $silentKey = null): void
{
$this->keysByQuotedStatus = [];

if ($originalContent === null) {
return;
}
Expand All @@ -156,40 +171,70 @@ protected function resolveOriginalContentSpacingAndOrder(?string $originalConten

$this->hasOpeningBracket = (bool) Strings::match($originalContent, '#^\(#');
$this->hasClosingBracket = (bool) Strings::match($originalContent, '#\)$#');

foreach ($this->orderedVisibleItems as $orderedVisibleItem) {
$this->keysByQuotedStatus[$orderedVisibleItem] = $this->isKeyQuoted(
$originalContent,
$orderedVisibleItem,
$silentKey
);
}

$this->silentKey = $silentKey;
$this->isSilentKeyExplicit = (bool) Strings::contains($originalContent, sprintf('%s=', $silentKey));
}

protected function resolveIsValueQuoted(string $originalContent, $value): bool
protected function printValueWithOptionalQuotes(string $key, ...$values): string
{
if ($value === null) {
return false;
// pick first non-null value
foreach ($values as $value) {
if ($value === null) {
continue;
}

break;
}

if (! is_string($value)) {
return false;
if (is_array($value)) {
return $this->printArrayItem($value, $key);
}

// @see https://regex101.com/r/VgvK8C/3/
$quotedNamePattern = sprintf('#"%s"#', preg_quote($value, '#'));
$keyPart = $this->createKeyPart($key);

return (bool) Strings::match($originalContent, $quotedNamePattern);
// quote by default
if (! isset($this->keysByQuotedStatus[$key]) || (isset($this->keysByQuotedStatus[$key]) && $this->keysByQuotedStatus[$key])) {
return sprintf('%s"%s"', $keyPart, $value);
}

return $keyPart . $value;
}

protected function printWithOptionalQuotes(string $name, $value, bool $isQuoted, bool $isExplicit = true): string
private function isKeyQuoted(string $originalContent, string $key, ?string $silentKey): bool
{
$content = '';
if ($isExplicit) {
$content = $name . '=';
$escapedKey = preg_quote($key, '#');

$quotedKeyPattern = $this->createQuotedKeyPattern($silentKey, $key, $escapedKey);
if ((bool) Strings::match($originalContent, $quotedKeyPattern)) {
return true;
}

if (is_array($value)) {
return $content . $this->printArrayItem($value);
// @see https://regex101.com/r/VgvK8C/5/
$quotedArrayPattern = sprintf('#%s=\{"(.*)"\}|\{"(.*)"\}#', $escapedKey);

return (bool) Strings::match($originalContent, $quotedArrayPattern);
}

private function createKeyPart(?string $key): string
{
if ($key === null) {
return '';
}

if ($isQuoted) {
return $content . sprintf('"%s"', $value);
if ($key === $this->silentKey && ! $this->isSilentKeyExplicit) {
return '';
}

return $content . sprintf('%s', $value);
return $key . '=';
}

/**
Expand All @@ -215,4 +260,28 @@ private function printTagValueNodesSeparatedByComma(array $tagValueNodes): strin

return implode(', ', $itemsAsStrings);
}

private function isValueWithoutQuotes(?string $key): bool
{
if ($key === null) {
return false;
}

if (! array_key_exists($key, $this->keysByQuotedStatus)) {
return false;
}

return ! $this->keysByQuotedStatus[$key];
}

private function createQuotedKeyPattern(?string $silentKey, string $key, string $escapedKey): string
{
if ($silentKey === $key) {
// @see https://regex101.com/r/VgvK8C/4/
return sprintf('#(%s=")|\("#', $escapedKey);
}

// @see https://regex101.com/r/VgvK8C/3/
return sprintf('#%s="#', $escapedKey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ public function __toString(): string
$contentItems = [];

if ($this->repositoryClass !== null) {
$contentItems['repositoryClass'] = sprintf('repositoryClass="%s"', $this->repositoryClass);
$contentItems['repositoryClass'] = $this->printValueWithOptionalQuotes(
'repositoryClass',
$this->repositoryClass
);
}

if ($this->readOnly !== null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function __construct(string $class, ?string $originalContent = null)

public function __toString(): string
{
return sprintf('(class="%s")', $this->class);
return '(' . $this->printValueWithOptionalQuotes('class', $this->class) . ')';
}

public function getShortName(): string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ final class SymfonyRouteTagValueNode extends AbstractTagValueNode implements Sho
*/
private $host;

/**
* @var bool
*/
private $isPathExplicit = true;

/**
* @var string[]
*/
Expand Down Expand Up @@ -74,11 +69,6 @@ final class SymfonyRouteTagValueNode extends AbstractTagValueNode implements Sho
*/
private $condition;

/**
* @var bool
*/
private $isNameQuoted = true;

/**
* @param string[] $localizedPaths
* @param string[] $methods
Expand Down Expand Up @@ -111,20 +101,17 @@ public function __construct(

// @todo make generic to abstrat class
if ($originalContent !== null) {
$this->isPathExplicit = (bool) Strings::contains($originalContent, 'path=');

$this->resolveOriginalContentSpacingAndOrder($originalContent);
$this->resolveOriginalContentSpacingAndOrder($originalContent, 'path');

// default value without key
if ($this->shouldAddImplicitPaths()) {
// add path as first item
$this->orderedVisibleItems = array_merge(['path'], (array) $this->orderedVisibleItems);
}

// @todo use generic approach
$matches = Strings::match($originalContent, '#requirements={(.*?)(?<separator>(=|:))(.*)}#');
$this->requirementsKeyValueSeparator = $matches['separator'] ?? '=';

$this->isNameQuoted = $this->resolveIsValueQuoted($originalContent, $name);
}

$this->host = $host;
Expand All @@ -134,11 +121,11 @@ public function __construct(
public function __toString(): string
{
$contentItems = [
'path' => $this->createPath(),
'path' => $this->printValueWithOptionalQuotes('path', $this->path, $this->localizedPaths),
];

if ($this->name) {
$contentItems['name'] = $this->printWithOptionalQuotes('name', $this->name, $this->isNameQuoted);
$contentItems['name'] = $this->printValueWithOptionalQuotes('name', $this->name);
}

if ($this->methods !== []) {
Expand Down Expand Up @@ -183,21 +170,6 @@ public function getShortName(): string
return '@Route';
}

private function createPath(): string
{
if ($this->isPathExplicit) {
return sprintf('path="%s"', $this->path);
}

if ($this->path !== null) {
return sprintf('"%s"', $this->path);
}

$localizedPaths = $this->printArrayItem($this->localizedPaths);

return Strings::replace($localizedPaths, '#:#', ': ');
}

private function shouldAddImplicitPaths(): bool
{
return ($this->path || $this->localizedPaths) && ! in_array('path', (array) $this->orderedVisibleItems, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Rector\BetterPhpDocParser\PhpDocNode\Symfony\Validator\Constraints;

use Nette\Utils\Strings;
use Rector\BetterPhpDocParser\Contract\PhpDocNode\ShortNameAwareTagInterface;
use Rector\BetterPhpDocParser\Contract\PhpDocNode\TypeAwareTagValueNodeInterface;
use Rector\Symfony\PhpDocParser\Ast\PhpDoc\AbstractConstraintTagValueNode;
Expand All @@ -29,16 +28,6 @@ final class AssertChoiceTagValueNode extends AbstractConstraintTagValueNode impl
*/
private $choices;

/**
* @var bool
*/
private $isChoicesExplicit = true;

/**
* @var bool
*/
private $isChoiceQuoted = false;

/**
* @param mixed[]|string|null $callback
* @param mixed[]|string|null $choices
Expand All @@ -49,13 +38,7 @@ public function __construct($groups, $callback, ?bool $strict, ?string $original
$this->strict = $strict;
$this->choices = $choices;

if ($originalContent !== null) {
$this->isChoicesExplicit = (bool) Strings::contains($originalContent, 'choices=');

$this->resolveAreQuotedChoices($originalContent, $choices);
}

$this->resolveOriginalContentSpacingAndOrder($originalContent);
$this->resolveOriginalContentSpacingAndOrder($originalContent, 'choices');

parent::__construct($groups);
}
Expand All @@ -67,7 +50,7 @@ public function __toString(): string
if ($this->callback) {
$contentItems['callback'] = $this->createCallback();
} elseif ($this->choices) {
$contentItems[] = $this->createChoices();
$contentItems['choices'] = $this->printValueWithOptionalQuotes('choices', $this->choices);
}

if ($this->strict !== null) {
Expand All @@ -94,26 +77,6 @@ public function getShortName(): string
return '@Assert\Choice';
}

private function createChoices(): string
{
$content = '';
if ($this->isChoicesExplicit) {
$content .= 'choices=';
}

if (is_string($this->choices)) {
return $content . $this->choices;
}

assert(is_array($this->choices));

if ($this->isChoiceQuoted) {
return $content . $this->printArrayItem($this->choices);
}

return $content . $this->printArrayItemWithoutQuotes($this->choices);
}

private function createCallback(): string
{
if (is_array($this->callback)) {
Expand All @@ -122,20 +85,4 @@ private function createCallback(): string

return sprintf('callback="%s"', $this->callback);
}

private function resolveAreQuotedChoices(string $originalContent, $choices): void
{
if ($choices === null) {
return;
}

if (is_array($choices)) {
$choices = implode('", "', $choices);
}

// @see https://regex101.com/r/VgvK8C/3/
$quotedChoicePattern = sprintf('#\(\{"%s"\}\)#', preg_quote($choices, '#'));

$this->isChoiceQuoted = (bool) Strings::match($originalContent, $quotedChoicePattern);
}
}
Loading