Skip to content

Commit

Permalink
feat: add phpDoc support for fully_qualified_strict_types fixer (PH…
Browse files Browse the repository at this point in the history
…P-CS-Fixer#5620)

Co-authored-by: JADRNT01 <jadrnt01@dixonscarphone.com>
Co-authored-by: Greg Korba <greg@codito.dev>
  • Loading branch information
3 people authored and danog committed Feb 2, 2024
1 parent a2abf9c commit 1b4fdcd
Show file tree
Hide file tree
Showing 5 changed files with 518 additions and 58 deletions.
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
Loading

0 comments on commit 1b4fdcd

Please sign in to comment.