Skip to content
Open
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 .github/workflows/bare_run.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php_version: ['7.4', '8.0', '8.2']
php_version: ['7.4', '8.0', '8.2', '8.4']

steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/code_analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:

-
name: 'Check Active Classes'
run: vendor/bin/class-leak check src --ansi
run: vendor/bin/class-leak check src --ansi --skip-type "PHPStan\Type\DynamicMethodReturnTypeExtension" --skip-type "PHPStan\Type\DynamicFunctionReturnTypeExtension"

name: ${{ matrix.actions.name }}
runs-on: ubuntu-latest
Expand Down
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,24 @@ parameters:

<br>

Want sharper type inference? The return type extensions are **disabled by default** — enable the ones that fit your stack:

```yaml
parameters:
symfonyReturnType: true
laravelReturnType: true
pathStrings: true
```

`symfonyReturnType` resolves `$container->get(SomeService::class)` to `SomeService` and Symfony Finder's `$splFileInfo->getRealPath()` to `string`. `laravelReturnType` does the same for Laravel's `$container->make(SomeService::class)`. `pathStrings` narrows `getcwd()`, `dirname()` and `realpath()` to `string`:

```php
$service = $container->get(SomeService::class);
// $service is now known as SomeService, instead of plain object
```

<br>

But at start, make baby steps with one rule at a time:

Jump to: [Symfony-specific rules](#3-symfony-specific-rules), [Doctrine-specific rules](#2-doctrine-specific-rules), [PHPUnit-specific rules](#4-phpunit-specific-rules) or [PHPUnit mock rules](#5-phpunit-mock-rules).
Expand Down Expand Up @@ -2966,4 +2984,51 @@ final class SomeTest extends TestCase

<br>

## 6. Type Extensions and Error Formatter

These extensions were merged from the now-deprecated [`symplify/phpstan-extensions`](https://github.com/symplify/phpstan-extensions)
package. They load automatically once `phpstan/extension-installer` is set up - no extra
configuration is needed.

<br>

### `symplify` Error Formatter

A compact error format with pre-escaped, regex-ready messages that are easy to copy into
your `ignoreErrors` list. File paths are printed with line numbers and stay clickable in
the terminal (works best with [anthraxx/intellij-awesome-console](https://github.com/anthraxx/intellij-awesome-console)).

Enable it in your `phpstan.neon`:

```yaml
parameters:
errorFormat: symplify
```

or on the command line:

```bash
vendor/bin/phpstan analyse --error-format symplify
```

<br>

### Type Extensions

Always-on return type extensions that sharpen PHPStan inference for common framework calls:

* **`ContainerGetReturnTypeExtension`** - `$container->get(SomeService::class)` returns
`SomeService` instead of plain `object` (Symfony `ContainerInterface`).

* **`LaravelContainerMakeTypeExtension`** - `$container->make(SomeService::class)` and
`->get(SomeService::class)` return `SomeService` (Laravel `Illuminate\Container\Container`).

* **`SplFileInfoTolerantReturnTypeExtension`** - `$splFileInfo->getRealPath()` returns
`string` instead of `string|false`, as Symfony Finder only yields existing files.

* **`NativeFunctionReturnTypeExtension`** - `getcwd()`, `dirname()` and `realpath()` return
`string` instead of `string|false`.

<br>

Happy coding!
9 changes: 9 additions & 0 deletions composer-dependency-analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
// optional classes
->ignoreUnknownClasses(['Symfony\Component\ExpressionLanguage\Expression'])

// windows-only function used in Terminal helper
->ignoreUnknownFunctions(['sapi_windows_vt100_support'])

// used only by the Symfony Finder SplFileInfo return type extension
->ignoreErrorsOnPackage('symfony/finder', [ErrorType::SHADOW_DEPENDENCY])

// extension that runs on Laravel Container only
->ignoreErrorsOnPackage('illuminate/container', [ErrorType::DEV_DEPENDENCY_IN_PROD])

// already in phpstan/phpstan
->ignoreErrorsOnPackage('nikic/php-parser', [ErrorType::DEV_DEPENDENCY_IN_PROD])
->ignoreErrorsOnPackage('symfony/routing', [ErrorType::SHADOW_DEPENDENCY])
Expand Down
11 changes: 6 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"description": "Set of Symplify rules for PHPStan",
"license": "MIT",
"require": {
"php": ">=8.4",
"webmozart/assert": " ^2.0",
"php": "^8.4",
"webmozart/assert": "^2.0",
"phpstan/phpstan": "^2.2",
"nette/utils": "^4.1",
"phpstan/phpdoc-parser": "^2.3"
Expand All @@ -14,11 +14,11 @@
"nikic/php-parser": "^5.7",
"phpunit/phpunit": "^13.0",
"symfony/framework-bundle": "^6.2",
"symplify/easy-coding-standard": "^13.0",
"illuminate/container": "^11.0",
"phpecs/phpecs": "^2.2",
"tomasvotruba/class-leak": "^2.1",
"rector/rector": "^2.4",
"phpstan/extension-installer": "^1.4",
"symplify/phpstan-extensions": "^12.0",
"tomasvotruba/unused-public": "^2.2",
"tomasvotruba/type-coverage": "^2.2",
"shipmonk/composer-dependency-analyser": "^1.8",
Expand Down Expand Up @@ -61,7 +61,8 @@
"includes": [
"config/services/services.neon",
"config/ctor-rules.neon",
"config/mock-rules.neon"
"config/mock-rules.neon",
"config/phpstan-extensions.neon"
]
}
}
Expand Down
40 changes: 40 additions & 0 deletions config/phpstan-extensions.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# return type extensions are disabled by default; enable in your phpstan.neon with:
#
# parameters:
# symfonyReturnType: true
# laravelReturnType: true
# pathStrings: true

parameters:
symfonyReturnType: false
laravelReturnType: false
pathStrings: false

parametersSchema:
symfonyReturnType: bool()
laravelReturnType: bool()
pathStrings: bool()

conditionalTags:
# Symfony Container::get($1) => $1 type
Symplify\PHPStanRules\ReturnTypeExtension\Symfony\ContainerGetReturnTypeExtension:
phpstan.broker.dynamicMethodReturnTypeExtension: %symfonyReturnType%
# Symfony Finder SplFileInfo::getRealPath() => string type
Symplify\PHPStanRules\ReturnTypeExtension\Symfony\SplFileInfoTolerantReturnTypeExtension:
phpstan.broker.dynamicMethodReturnTypeExtension: %symfonyReturnType%
# Laravel Container::make($1) => $1 type
Symplify\PHPStanRules\ReturnTypeExtension\Laravel\LaravelContainerMakeTypeExtension:
phpstan.broker.dynamicMethodReturnTypeExtension: %laravelReturnType%
# getcwd()/dirname()/realpath() => always "string"
Symplify\PHPStanRules\ReturnTypeExtension\NativeFunctionReturnTypeExtension:
phpstan.broker.dynamicFunctionReturnTypeExtension: %pathStrings%

services:
# use with "errorFormat: symplify" in CLI/config
errorFormatter.symplify:
class: Symplify\PHPStanRules\ErrorFormatter\SymplifyErrorFormatter

- Symplify\PHPStanRules\ReturnTypeExtension\Symfony\ContainerGetReturnTypeExtension
- Symplify\PHPStanRules\ReturnTypeExtension\Symfony\SplFileInfoTolerantReturnTypeExtension
- Symplify\PHPStanRules\ReturnTypeExtension\Laravel\LaravelContainerMakeTypeExtension
- Symplify\PHPStanRules\ReturnTypeExtension\NativeFunctionReturnTypeExtension
8 changes: 7 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
includes:
- config/services/services.neon
- config/naming-rules.neon
- config/phpstan-extensions.neon

parameters:
treatPhpDocTypesAsCertain: false
Expand Down Expand Up @@ -46,7 +47,12 @@ parameters:

-
message: '#Generator expects value type array<string, mixed>, list<bool\|float\|int\|list<string>\|PHPStan\\TrinaryLogic\|string\|null> given#'
path: tests/ReturnTypeExtension/NodeGetAttributeTypeExtension/NodeGetAttributeTypeExtensionTest.php
path: tests/ReturnTypeExtension/*

# PHPStan testing helper, intentionally extended
-
message: '#is not covered by backward compatibility promise#'
path: tests/ErrorFormatter/SymplifyErrorFormatterTest.php

- '#Although PHPStan\\Node\\InClassNode is covered by backward compatibility promise, this instanceof assumption might break because (.*?) not guaranteed to always stay the same#'
- '#PHPStan\\DependencyInjection\\NeonAdapter#'
Expand Down
153 changes: 153 additions & 0 deletions src/Console/Terminal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Console;

/**
* Copied mostly from symfony/console, to avoid hard dependency for single method
* @see https://github.com/symfony/symfony/blob/e16aea4e88bfecec4986bf7a693f84a86c074109/src/Symfony/Component/Console/Terminal.php#L86
*/
final class Terminal
{
/**
* @var array<int, array<string>>
*/
private const array DESCRIPTORSPEC = [
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];

private static ?int $width = null;

private static ?bool $stty = null;

public static function getWidth(): int
{
$width = \getenv('COLUMNS');
if ($width !== \false) {
return (int) \trim($width);
}

if (self::$width === null) {
self::initDimensions();
}

return self::$width ?: 80;
}

private static function hasSttyAvailable(): bool
{
if (self::$stty !== null) {
return self::$stty;
}

if (! \function_exists('exec')) {
return \false;
}

\exec('stty 2>&1', $output, $exitcode);
return self::$stty = $exitcode === 0;
}

private static function initDimensions(): void
{
$consoleMode = self::getConsoleMode();

if ('\\' === \DIRECTORY_SEPARATOR) {
$width = self::getAnsiconWidth();
if ($width !== null) {
self::$width = $width;
} elseif (! self::hasVt100Support() && self::hasSttyAvailable()) {
self::initDimensionsUsingStty();
} elseif ($consoleMode) {
self::$width = $consoleMode[0];
}
} else {
self::initDimensionsUsingStty();
}
}

private static function hasVt100Support(): bool
{
if (! \function_exists('sapi_windows_vt100_support')) {
return \false;
}

$stdoutStream = \fopen('php://stdout', 'w');
if ($stdoutStream === \false) {
return \false;
}

return \sapi_windows_vt100_support($stdoutStream);
}

private static function initDimensionsUsingStty(): void
{
$sttyColumns = self::getSttyColumns();

if ($sttyColumns) {
if (\preg_match('#rows.(\d+);.columns.(\d+);#i', $sttyColumns, $matches)) {
self::$width = (int) $matches[2];
} elseif (\preg_match('#;.(\d+).rows;.(\d+).columns#i', $sttyColumns, $matches)) {
self::$width = (int) $matches[2];
}
}
}

/**
* @return array{0: int, 1: int}|null
*/
private static function getConsoleMode(): ?array
{
$info = self::readFromProcess('mode CON');
if ($info === null) {
return null;
}

if (! \preg_match('#--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n#', $info, $matches)) {
return null;
}

return [(int) $matches[2], (int) $matches[1]];
}

private static function getSttyColumns(): ?string
{
return self::readFromProcess('stty -a | grep columns');
}

private static function readFromProcess(string $command): ?string
{
if (! \function_exists('proc_open')) {
return null;
}

$process = \proc_open($command, self::DESCRIPTORSPEC, $pipes, null, null, [
'suppress_errors' => \true,
]);
if (! \is_resource($process)) {
return null;
}

$info = \stream_get_contents($pipes[1]);
\fclose($pipes[1]);
\fclose($pipes[2]);
\proc_close($process);
return $info;
}

private static function getAnsiconWidth(): ?int
{
$ansicon = \getenv('ANSICON');
if (! is_string($ansicon)) {
return null;
}

if (\preg_match('#^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$#', \trim($ansicon), $matches)) {
return (int) $matches[1];
}

return null;
}
}
12 changes: 12 additions & 0 deletions src/Enum/ResultStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Enum;

final class ResultStatus
{
public const int SUCCESS = 0;

public const int FAILURE = 1;
}
Loading
Loading