Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add phpDoc support for fully_qualified_strict_types fixer #5620

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 22 additions & 6 deletions doc/rules/import/fully_qualified_strict_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,32 @@ Example #1
use Foo\Bar;
use Foo\Bar\Baz;

/**
- * @see \Foo\Bar\Baz
+ * @see Baz
*/
class SomeClass
{
- public function doX(\Foo\Bar $foo): \Foo\Bar\Baz
+ public function doX(Bar $foo): Baz
{
/**
- * @var \Foo\Bar\Baz
+ * @var Baz
*/
public $baz;

/**
- * @param \Foo\Bar\Baz $baz
+ * @param Baz $baz
*/
public function __construct($baz) {
$this->baz = $baz;
}

- public function doY(Foo\NotImported $u, \Foo\NotImported $v)
+ public function doY(Foo\NotImported $u, Foo\NotImported $v)
{
/**
- * @return \Foo\Bar\Baz
+ * @return Baz
*/
public function getBaz() {
return $this->baz;
}
}

Expand Down
2 changes: 1 addition & 1 deletion phpstan.dist.neon
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ parameters:
-
message: '#^Method PhpCsFixer\\Tests\\.+::provide.+Cases\(\) return type has no value type specified in iterable type iterable\.$#'
path: tests
count: 1120
count: 1113

-
message: '#Call to static method .+ with .+ will always evaluate to true.$#'
Expand Down
170 changes: 120 additions & 50 deletions src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
Expand All @@ -31,6 +32,8 @@

/**
* @author VeeWee <toonverwerft@gmail.com>
* @author Tomas Jadrny <developer@tomasjadrny.cz>
* @author Greg Korba <greg@codito.dev>
*/
final class FullyQualifiedStrictTypesFixer extends AbstractFixer implements ConfigurableFixerInterface
{
Expand All @@ -45,14 +48,28 @@ public function getDefinition(): FixerDefinitionInterface
use Foo\Bar;
use Foo\Bar\Baz;

/**
* @see \Foo\Bar\Baz
*/
class SomeClass
{
public function doX(\Foo\Bar $foo): \Foo\Bar\Baz
{
/**
* @var \Foo\Bar\Baz
*/
public $baz;

/**
* @param \Foo\Bar\Baz $baz
*/
public function __construct($baz) {
$this->baz = $baz;
}

public function doY(Foo\NotImported $u, \Foo\NotImported $v)
{
/**
* @return \Foo\Bar\Baz
*/
public function getBaz() {
return $this->baz;
}
}
'
Expand Down Expand Up @@ -86,7 +103,7 @@ public function getPriority(): int

public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_FUNCTION);
return $tokens->isAnyTokenKindsFound([T_FUNCTION, T_DOC_COMMENT]);
}

protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
Expand Down Expand Up @@ -119,6 +136,10 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
if ($tokens[$index]->isGivenKind(T_FUNCTION)) {
$this->fixFunction($functionsAnalyzer, $tokens, $index, $uses, $namespaceName);
}

if ($tokens[$index]->isGivenKind(T_DOC_COMMENT)) {
$this->fixPhpDoc($tokens, $index, $uses, $namespaceName);
}
}
}
}
Expand All @@ -145,6 +166,40 @@ private function fixFunction(FunctionsAnalyzer $functionsAnalyzer, Tokens $token
}
}

/**
* @param array<string, string> $uses
*/
private function fixPhpDoc(Tokens $tokens, int $index, array $uses, string $namespaceName): void
{
$phpDoc = $tokens[$index];
$phpDocContent = $phpDoc->getContent();
Preg::matchAll('#@([^\s]+)\s+([^\s]+)#', $phpDocContent, $matches);

if ([] !== $matches) {
foreach ($matches[2] as $i => $typeName) {
if (!\in_array($matches[1][$i], ['param', 'return', 'see', 'throws', 'var'], true)) {
continue;
}

$shortTokens = $this->determineShortType($typeName, $uses, $namespaceName);

if (null !== $shortTokens) {
// Replace tag+type in order to avoid replacing type multiple times (when same type is used in multiple places)
$phpDocContent = str_replace(
$matches[0][$i],
'@'.$matches[1][$i].' '.implode('', array_map(
static fn (Token $token) => $token->getContent(),
$shortTokens
)),
$phpDocContent
);
}
}

$tokens[$index] = new Token([T_DOC_COMMENT, $phpDocContent]);
}
}

/**
* @param array<string, string> $uses
*/
Expand All @@ -156,68 +211,83 @@ private function replaceByShortType(Tokens $tokens, TypeAnalysis $type, array $u
$typeStartIndex = $tokens->getNextMeaningfulToken($typeStartIndex);
}

$namespaceNameLength = \strlen($namespaceName);
$types = $this->getTypes($tokens, $typeStartIndex, $type->getEndIndex());

foreach ($types as $typeName => [$startIndex, $endIndex]) {
if ((new TypeAnalysis($typeName))->isReservedType()) {
return;
}

$withLeadingBackslash = str_starts_with($typeName, '\\');
if ($withLeadingBackslash) {
$typeName = substr($typeName, 1);
$shortType = $this->determineShortType($typeName, $uses, $namespaceName);

if (null !== $shortType) {
$tokens->overrideRange($startIndex, $endIndex, $shortType);
}
$typeNameLower = strtolower($typeName);
}
}

if (isset($uses[$typeNameLower]) && ($withLeadingBackslash || '' === $namespaceName)) {
// if the type without leading "\" equals any of the full "uses" long names, it can be replaced with the short one
$tokens->overrideRange($startIndex, $endIndex, $this->namespacedStringToTokens($uses[$typeNameLower]));
/**
* Determines short type based on FQCN, current namespace and imports (`use` declarations).
*
* @param array<string, string> $uses
*
* @return null|Token[]
*/
private function determineShortType(string $typeName, array $uses, string $namespaceName): ?array
{
$withLeadingBackslash = str_starts_with($typeName, '\\');
if ($withLeadingBackslash) {
$typeName = substr($typeName, 1);
}
$typeNameLower = strtolower($typeName);
$namespaceNameLength = \strlen($namespaceName);

continue;
}
if (isset($uses[$typeNameLower]) && ($withLeadingBackslash || '' === $namespaceName)) {
// if the type without leading "\" equals any of the full "uses" long names, it can be replaced with the short one
return $this->namespacedStringToTokens($uses[$typeNameLower]);
}

if ('' === $namespaceName) {
foreach ($uses as $useShortName) {
if (strtolower($useShortName) === $typeNameLower) {
continue 2;
}
if ('' === $namespaceName) {
// if we are in the global namespace and the type is not imported the leading '\' can be removed (TODO nice config candidate)
foreach ($uses as $useShortName) {
if (strtolower($useShortName) === $typeNameLower) {
return null;
}
}

// if we are in the global namespace and the type is not imported,
// we enforce/remove leading backslash (depending on the configuration)
if (true === $this->configuration['leading_backslash_in_global_namespace']) {
if (!$withLeadingBackslash && !isset($uses[$typeNameLower])) {
$tokens->overrideRange(
$startIndex,
$endIndex,
$this->namespacedStringToTokens($typeName, true)
);
}
} else {
$tokens->overrideRange($startIndex, $endIndex, $this->namespacedStringToTokens($typeName));
// if we are in the global namespace and the type is not imported,
// we enforce/remove leading backslash (depending on the configuration)
if (true === $this->configuration['leading_backslash_in_global_namespace']) {
if (!$withLeadingBackslash && !isset($uses[$typeNameLower])) {
return $this->namespacedStringToTokens($typeName, true);
}
} elseif (!str_contains($typeName, '\\')) {
// If we're NOT in the global namespace, there's no related import,
// AND used type is from global namespace, then it can't be shortened.
continue;
} elseif ($typeNameLower !== $namespaceName && str_starts_with($typeNameLower, $namespaceName.'\\')) {
// if the type starts with namespace and the type is not the same as the namespace it can be shortened
$typeNameShort = substr($typeName, $namespaceNameLength + 1);

// if short names are the same, but long one are different then it cannot be shortened
foreach ($uses as $useLongName => $useShortName) {
if (
strtolower($typeNameShort) === strtolower($useShortName)
&& strtolower($typeName) !== strtolower($useLongName)
) {
continue 2;
}
} else {
return $this->namespacedStringToTokens($typeName);
}
}
if (!str_contains($typeName, '\\')) {
// If we're NOT in the global namespace, there's no related import,
// AND used type is from global namespace, then it can't be shortened.
return null;
}
if ($typeNameLower !== $namespaceName && str_starts_with($typeNameLower, $namespaceName.'\\')) {
// if the type starts with namespace and the type is not the same as the namespace it can be shortened
$typeNameShort = substr($typeName, $namespaceNameLength + 1);

// if short names are the same, but long one are different then it cannot be shortened
foreach ($uses as $useLongName => $useShortName) {
if (
strtolower($typeNameShort) === strtolower($useShortName)
&& strtolower($typeName) !== strtolower($useLongName)
) {
return null;
}

$tokens->overrideRange($startIndex, $endIndex, $this->namespacedStringToTokens($typeNameShort));
}

return $this->namespacedStringToTokens($typeNameShort);
}

return null;
}

/**
Expand Down