Skip to content

Commit

Permalink
Merge remote-tracking branch 'phpstan/1.10.x' into HEAD
Browse files Browse the repository at this point in the history
  • Loading branch information
janedbal committed Jan 4, 2024
2 parents 666b99e + 880ffbc commit f5c0183
Show file tree
Hide file tree
Showing 16 changed files with 230 additions and 76 deletions.
2 changes: 2 additions & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ parameters:
checkUnresolvableParameterTypes: true
readOnlyByPhpDoc: true
phpDocParserRequireWhitespaceBeforeDescription: true
phpDocParserIncludeLines: true
enableIgnoreErrorsWithinPhpDocs: true
runtimeReflectionRules: true
notAnalysedTrait: true
curlSetOptTypes: true
Expand Down
5 changes: 5 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ parameters:
checkUnresolvableParameterTypes: false
readOnlyByPhpDoc: false
phpDocParserRequireWhitespaceBeforeDescription: false
phpDocParserIncludeLines: false
enableIgnoreErrorsWithinPhpDocs: false
runtimeReflectionRules: false
notAnalysedTrait: false
curlSetOptTypes: false
Expand Down Expand Up @@ -389,6 +391,8 @@ services:
arguments:
requireWhitespaceBeforeDescription: %featureToggles.phpDocParserRequireWhitespaceBeforeDescription%
preserveTypeAliasesWithInvalidTypes: true
usedAttributes:
lines: %featureToggles.phpDocParserIncludeLines%

-
class: PHPStan\PhpDoc\ConstExprParserFactory
Expand Down Expand Up @@ -1826,6 +1830,7 @@ services:
arguments:
parser: @currentPhpVersionPhpParser
lexer: @currentPhpVersionLexer
enableIgnoreErrorsWithinPhpDocs: %featureToggles.enableIgnoreErrorsWithinPhpDocs%
autowired: no

currentPhpVersionSimpleParser:
Expand Down
2 changes: 2 additions & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ parametersSchema:
checkUnresolvableParameterTypes: bool()
readOnlyByPhpDoc: bool()
phpDocParserRequireWhitespaceBeforeDescription: bool()
phpDocParserIncludeLines: bool()
enableIgnoreErrorsWithinPhpDocs: bool()
runtimeReflectionRules: bool()
notAnalysedTrait: bool()
curlSetOptTypes: bool()
Expand Down
54 changes: 47 additions & 7 deletions src/Parser/RichParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public function __construct(
private NameResolver $nameResolver,
private Container $container,
private IgnoreLexer $ignoreLexer,
private bool $enableIgnoreErrorsWithinPhpDocs,
)
{
}
Expand Down Expand Up @@ -150,14 +151,26 @@ private function getLinesToIgnore(array $tokens): array
$text = $token[1];
$isNextLine = str_contains($text, '@phpstan-ignore-next-line');
$isCurrentLine = str_contains($text, '@phpstan-ignore-line');
if ($isNextLine) {
$line++;
}
if ($isNextLine || $isCurrentLine) {
$line += substr_count($token[1], "\n");

$lines[$line] = null;
continue;
if ($this->enableIgnoreErrorsWithinPhpDocs) {
$lines = $lines +
$this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-next-line', true) +
$this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-line');

if ($isNextLine || $isCurrentLine) {
continue;
}

} else {
if ($isNextLine) {
$line++;
}
if ($isNextLine || $isCurrentLine) {
$line += substr_count($token[1], "\n");

$lines[$line] = null;
continue;
}
}

$ignorePos = strpos($text, '@phpstan-ignore');
Expand Down Expand Up @@ -206,6 +219,33 @@ private function getLinesToIgnore(array $tokens): array
];
}

/**
* @return array<int, null>
*/
private function getLinesToIgnoreForTokenByIgnoreComment(
string $tokenText,
int $tokenLine,
string $ignoreComment,
bool $ignoreNextLine = false,
): array
{
$lines = [];
$positionsOfIgnoreComment = [];
$offset = 0;

while (($pos = strpos($tokenText, $ignoreComment, $offset)) !== false) {
$positionsOfIgnoreComment[] = $pos;
$offset = $pos + 1;
}

foreach ($positionsOfIgnoreComment as $pos) {
$line = $tokenLine + substr_count(substr($tokenText, 0, $pos), "\n") + ($ignoreNextLine ? 1 : 0);
$lines[$line] = null;
}

return $lines;
}

/**
* @return non-empty-list<string>
* @throws IgnoreParseException
Expand Down
29 changes: 11 additions & 18 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -435,29 +435,18 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit

$parameters = [];
$isVariadic = false;
$returnType = null;
$phpDocReturnType = null;
$nativeReturnType = null;
$returnTypes = [];
$phpDocReturnTypes = [];
$nativeReturnTypes = [];

foreach ($acceptors as $acceptor) {
if ($returnType === null) {
$returnType = $acceptor->getReturnType();
} else {
$returnType = TypeCombinator::union($returnType, $acceptor->getReturnType());
}
$returnTypes[] = $acceptor->getReturnType();

if ($acceptor instanceof ParametersAcceptorWithPhpDocs) {
if ($phpDocReturnType === null) {
$phpDocReturnType = $acceptor->getPhpDocReturnType();
} else {
$phpDocReturnType = TypeCombinator::union($phpDocReturnType, $acceptor->getPhpDocReturnType());
}
$phpDocReturnTypes[] = $acceptor->getPhpDocReturnType();
}
if ($acceptor instanceof ParametersAcceptorWithPhpDocs) {
if ($nativeReturnType === null) {
$nativeReturnType = $acceptor->getNativeReturnType();
} else {
$nativeReturnType = TypeCombinator::union($nativeReturnType, $acceptor->getNativeReturnType());
}
$nativeReturnTypes[] = $acceptor->getNativeReturnType();
}
$isVariadic = $isVariadic || $acceptor->isVariadic();

Expand Down Expand Up @@ -524,6 +513,10 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit
}
}

$returnType = TypeCombinator::union(...$returnTypes);
$phpDocReturnType = $phpDocReturnTypes === [] ? null : TypeCombinator::union(...$phpDocReturnTypes);
$nativeReturnType = $nativeReturnTypes === [] ? null : TypeCombinator::union(...$nativeReturnTypes);

return new FunctionVariantWithPhpDocs(
TemplateTypeMap::createEmpty(),
null,
Expand Down
10 changes: 5 additions & 5 deletions src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@ public function getNodeType(): string

public function processNode(Node $node, Scope $scope): array
{
$docComment = $node->getDocComment();
if ($docComment === null) {
return [];
}

if ($node instanceof Node\Stmt\ClassMethod) {
$functionName = $node->name->name;
} elseif ($node instanceof Node\Stmt\Function_) {
Expand All @@ -55,6 +50,11 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$docComment = $node->getDocComment();
if ($docComment === null) {
return [];
}

$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$scope->isInClass() ? $scope->getClassReflection()->getName() : null,
Expand Down
4 changes: 3 additions & 1 deletion src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ public function processNode(Node $node, Scope $scope): array
$errors[] = RuleErrorBuilder::message(sprintf(
'Unknown PHPDoc tag: %s',
$phpDocTag->name,
))->identifier('phpDoc.phpstanTag')->build();
))
->line(PhpDocLineHelper::detectLine($node, $phpDocTag))
->identifier('phpDoc.phpstanTag')->build();
}

return $errors;
Expand Down
8 changes: 6 additions & 2 deletions src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ public function processNode(Node $node, Scope $scope): array
$phpDocTag->name,
$phpDocTag->value->alias,
$this->trimExceptionMessage($phpDocTag->value->type->getException()->getMessage()),
))->identifier('phpDoc.parseError')->build();
))
->line(PhpDocLineHelper::detectLine($node, $phpDocTag))
->identifier('phpDoc.parseError')->build();

continue;
} elseif (!($phpDocTag->value instanceof InvalidTagValueNode)) {
Expand All @@ -102,7 +104,9 @@ public function processNode(Node $node, Scope $scope): array
$phpDocTag->name,
$phpDocTag->value->value,
$this->trimExceptionMessage($phpDocTag->value->exception->getMessage()),
))->identifier('phpDoc.parseError')->build();
))
->line(PhpDocLineHelper::detectLine($node, $phpDocTag))
->identifier('phpDoc.parseError')->build();
}

return $errors;
Expand Down
8 changes: 4 additions & 4 deletions src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ public function getNodeType(): string

public function processNode(Node $node, Scope $scope): array
{
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
return []; // is handled by virtual nodes
}

$docComment = $node->getDocComment();
if ($docComment === null) {
return [];
}

if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
return []; // is handled by virtual nodes
}

$functionName = null;
if ($scope->getFunction() !== null) {
$functionName = $scope->getFunction()->getName();
Expand Down
28 changes: 28 additions & 0 deletions src/Rules/PhpDoc/PhpDocLineHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PhpDoc;

use PhpParser\Node as PhpParserNode;
use PHPStan\PhpDocParser\Ast\Node as PhpDocNode;

class PhpDocLineHelper
{

/**
* This method returns exact line of e.g. `@param` tag in PHPDoc so that it can be used for precise error reporting
* - exact position is available only when bleedingEdge is enabled
* - otherwise, it falls back to given node start line
*/
public static function detectLine(PhpParserNode $node, PhpDocNode $phpDocNode): int
{
$phpDocTagLine = $phpDocNode->getAttribute('startLine');
$phpDoc = $node->getDocComment();

if ($phpDocTagLine === null || $phpDoc === null) {
return $node->getLine();
}

return $phpDoc->getStartLine() + $phpDocTagLine - 1;
}

}
24 changes: 19 additions & 5 deletions tests/PHPStan/Analyser/AnalyserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ public function testIgnoreNextLine(bool $reportUnmatchedIgnoredErrors): void
__DIR__ . '/data/ignore-next-line.php',
], true);
$this->assertCount($reportUnmatchedIgnoredErrors ? 4 : 3, $result);
foreach ([10, 30, 34] as $i => $line) {
foreach ([10, 20, 24] as $i => $line) {
$this->assertArrayHasKey($i, $result);
$this->assertInstanceOf(Error::class, $result[$i]);
$this->assertSame('Fail.', $result[$i]->getMessage());
Expand All @@ -521,8 +521,20 @@ public function testIgnoreNextLine(bool $reportUnmatchedIgnoredErrors): void

$this->assertArrayHasKey(3, $result);
$this->assertInstanceOf(Error::class, $result[3]);
$this->assertSame('No error to ignore is reported on line 38.', $result[3]->getMessage());
$this->assertSame(38, $result[3]->getLine());
$this->assertSame('No error to ignore is reported on line 28.', $result[3]->getMessage());
$this->assertSame(28, $result[3]->getLine());
}

public function testIgnoreNextLineLegacyBehaviour(): void
{
$result = $this->runAnalyser([], false, [__DIR__ . '/data/ignore-next-line-legacy.php'], true, false);

foreach ([10, 32, 36] as $i => $line) {
$this->assertArrayHasKey($i, $result);
$this->assertInstanceOf(Error::class, $result[$i]);
$this->assertSame('Fail.', $result[$i]->getMessage());
$this->assertSame($line, $result[$i]->getLine());
}
}

/**
Expand Down Expand Up @@ -611,9 +623,10 @@ private function runAnalyser(
bool $reportUnmatchedIgnoredErrors,
$filePaths,
bool $onlyFiles,
bool $enableIgnoreErrorsWithinPhpDocs = true,
): array
{
$analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors);
$analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors, $enableIgnoreErrorsWithinPhpDocs);

if (is_string($filePaths)) {
$filePaths = [$filePaths];
Expand Down Expand Up @@ -646,7 +659,7 @@ private function runAnalyser(
);
}

private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser
private function createAnalyser(bool $reportUnmatchedIgnoredErrors, bool $enableIgnoreErrorsWithinPhpDocs): Analyser
{
$ruleRegistry = new DirectRuleRegistry([
new AlwaysFailRule(),
Expand Down Expand Up @@ -695,6 +708,7 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser
new NameResolver(),
self::getContainer(),
new IgnoreLexer(),
$enableIgnoreErrorsWithinPhpDocs,
),
new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($fileTypeMapper, new ExprPrinter(new Printer())), $fileTypeMapper),
new RuleErrorTransformer(),
Expand Down
40 changes: 40 additions & 0 deletions tests/PHPStan/Analyser/data/ignore-next-line-legacy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace IgnoreNextLineLegacy;

class Foo
{

public function doFoo(): void
{
fail(); // reported

// @phpstan-ignore-next-line
fail();

/* @phpstan-ignore-next-line */
fail();

/** @phpstan-ignore-next-line */
fail();

/*
* @phpstan-ignore-next-line
*/
fail();

/**
* @phpstan-ignore-next-line
*
* This is the legacy behaviour, the next line is meant as next-non-comment-line
*/
fail();
fail(); // reported

// @phpstan-ignore-next-line
if (fail()) {
fail(); // reported
}
}

}
10 changes: 0 additions & 10 deletions tests/PHPStan/Analyser/data/ignore-next-line.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,6 @@ public function doFoo(): void

/** @phpstan-ignore-next-line */
fail();

/*
* @phpstan-ignore-next-line
*/
fail();

/**
* @phpstan-ignore-next-line
*/
fail();
fail(); // reported

// @phpstan-ignore-next-line
Expand Down
Loading

0 comments on commit f5c0183

Please sign in to comment.