diff --git a/.github/workflows/test-laravel.yml b/.github/workflows/test-laravel.yml
index 50851e44..967e3b88 100644
--- a/.github/workflows/test-laravel.yml
+++ b/.github/workflows/test-laravel.yml
@@ -1,4 +1,4 @@
-name: Test laravel projects
+name: Test Laravel app
on:
push:
@@ -19,13 +19,22 @@ on:
jobs:
build:
runs-on: ubuntu-latest
-
- name: Test Laravel
+ name: Test Laravel app
steps:
- name: Checkout code
uses: actions/checkout@v4
- - name: Test Laravel
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.3
+ coverage: none
+
+ - name: Build a Laravel project and run Psalm on it
run: |
./tests/Application/laravel-test.sh
+
+ - run: |
+ echo "Psalm analysis failed on a fresh Laravel project. Please consider updating baseline: tests/Application/laravel-test-baseline.xml"
+ if: ${{ failure() }}
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b84c7bcd..ee2b1047 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,7 +6,7 @@ on:
paths:
- '**.php'
- '**.stubphp'
- - '**.feature'
+ - '**.phpt'
- '**.yml'
- 'composer.json'
- '.github/workflows/test.yml'
@@ -14,7 +14,7 @@ on:
paths:
- '**.php'
- '**.stubphp'
- - '**.feature'
+ - '**.phpt'
- 'composer.json'
- '.github/workflows/test.yml'
schedule:
@@ -51,13 +51,10 @@ jobs:
strategy:
fail-fast: true
matrix:
- php: [8.4, 8.3, 8.2, 8.1]
- laravel: [^11.0, ^10.0]
- dependencies: [lowest, highest]
- exclude:
- - php: 8.1
- laravel: ^11.0
- name: Type P${{ matrix.php }} | L${{ matrix.laravel }} | ${{ matrix.dependencies == 'highest' && '↑' || '↓' }}
+ php: [8.4, 8.3, 8.2]
+ laravel: [^11.35]
+ dependencies: [highest]
+ name: Type test P${{ matrix.php }} | L${{ matrix.laravel }} | ${{ matrix.dependencies == 'highest' && '↑' || '↓' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
diff --git a/composer.json b/composer.json
index 9d685582..eb85f45c 100644
--- a/composer.json
+++ b/composer.json
@@ -11,33 +11,33 @@
],
"homepage": "https://github.com/psalm/psalm-plugin-laravel",
"require": {
- "php": "^8.1",
+ "php": "^8.2",
"ext-simplexml": "*",
- "barryvdh/laravel-ide-helper": "^2.13 || ^3.0",
- "illuminate/config": "^10.48 || ^11.0",
- "illuminate/container": "^10.48 || ^11.0",
- "illuminate/contracts": "^10.48 || ^11.0",
- "illuminate/database": "^10.48 || ^11.0",
- "illuminate/events": "^10.48 || ^11.0",
- "illuminate/http": "^10.48 || ^11.0",
- "illuminate/routing": "^10.48 || ^11.0",
- "illuminate/support": "^10.48 || ^11.0",
- "illuminate/view": "^10.48 || ^11.0",
- "nikic/php-parser": "^4.18 || ^5.0",
- "orchestra/testbench-core": "^8.22 || ^9.0",
- "symfony/console": "^6.0 || ^7.0",
- "symfony/finder": "^6.0 || ^7.0",
- "vimeo/psalm": "^5.20|^6"
+ "barryvdh/laravel-ide-helper": "~3.5.4",
+ "illuminate/config": "^11.35",
+ "illuminate/container": "^11.35",
+ "illuminate/contracts": "^11.35",
+ "illuminate/database": "^11.35",
+ "illuminate/events": "^11.35",
+ "illuminate/http": "^11.35",
+ "illuminate/routing": "^11.35",
+ "illuminate/support": "^11.35",
+ "illuminate/view": "^11.35",
+ "nikic/php-parser": "^5.0",
+ "orchestra/testbench-core": "^9.9",
+ "symfony/console": "^7.1",
+ "symfony/finder": "^7.1",
+ "vimeo/psalm": "^5.26 || ^6.0"
},
"require-dev": {
- "laravel/framework": "^10.48 || ^11.0",
- "phpunit/phpunit": "^10.5 || ^11.0",
+ "laravel/framework": "^11.35",
+ "phpunit/phpunit": "^10.5 || ^11.5",
"phpyh/psalm-tester": "^0.1.0",
"ramsey/collection": "^1.3",
- "rector/rector": "^1.0",
- "slevomat/coding-standard": "^8.8",
- "squizlabs/php_codesniffer": "*",
- "symfony/http-foundation": "^6.0 || ^7.0"
+ "rector/rector": "^2.0",
+ "slevomat/coding-standard": "^8.15",
+ "squizlabs/php_codesniffer": "^3.11",
+ "symfony/http-foundation": "^7.1"
},
"minimum-stability": "dev",
"prefer-stable": true,
@@ -68,14 +68,16 @@
"lint-fix": "phpcbf -n",
"psalm": "psalm --find-dead-code --find-unused-psalm-suppress --long-progress",
"psalm-set-baseline": "psalm --set-baseline=psalm-baseline.xml",
+ "rector": "./vendor/bin/rector",
+ "rector:dry": "./vendor/bin/rector --dry-run",
"test": [
"@lint",
"@psalm",
"@test:unit",
"@test:type"
],
+ "test:app": "./tests/Application/laravel-test.sh",
"test:type": "phpunit --testsuite=type",
- "test:unit": "phpunit --testsuite=unit",
- "rector": "./vendor/bin/rector --dry-run"
+ "test:unit": "phpunit --testsuite=unit"
}
}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 16945baa..6bb5e14e 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,25 +1,25 @@
-
-
- tests/Unit
-
-
- tests/Type
-
-
+ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
+ bootstrap="vendor/autoload.php"
+ cacheDirectory=".phpunit.cache"
+ executionOrder="depends,defects"
+ beStrictAboutCoverageMetadata="true"
+ beStrictAboutOutputDuringTests="true"
+ failOnRisky="true"
+ failOnWarning="true">
+
+
+ tests/Unit
+
+
+ tests/Type
+
+
-
- src
-
-
+
+ src
+
+
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 6a3abd58..650e3a45 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -1,18 +1,9 @@
-
-
-
- $abstract
-
-
+
config->get('auth.guards')]]>
-
- ?string
- |null]]>
-
config->get("auth.providers.{$provider}.model", null)]]>
config->get('auth.defaults.guard')]]>
@@ -20,14 +11,9 @@
- null
+
-
-
- name->parts]]>
-
-
getFqClasslikeName()]]>
@@ -35,29 +21,39 @@
getFqClasslikeName()]]>
- array
- array
- array
+
+
+
-
-
- value->name->parts]]>
-
-
- new TLiteralString($result)
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
- new TLiteralString($concrete)
+
diff --git a/psalm.xml b/psalm.xml
index 45b1c6ed..6f4b2b37 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -2,7 +2,7 @@
+
diff --git a/rector.php b/rector.php
index ed2ea39f..ce4b8aeb 100644
--- a/rector.php
+++ b/rector.php
@@ -2,7 +2,6 @@
use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector;
use Rector\CodingStyle\Rector\Assign\SplitDoubleAssignRector;
-use Rector\CodingStyle\Rector\Closure\StaticClosureRector;
use Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector;
use Rector\CodingStyle\Rector\If_\NullableCompareToNullRector;
use Rector\Config\RectorConfig;
@@ -11,24 +10,19 @@
use Rector\Php81\Rector\Array_\FirstClassCallableRector;
use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector;
use Rector\Php81\Rector\Property\ReadOnlyPropertyRector;
-use Rector\Strict\Rector\BooleanNot\BooleanInBooleanNotRuleFixerRector;
-use Rector\Strict\Rector\If_\BooleanInIfConditionRuleFixerRector;
return RectorConfig::configure()
->withPaths(['src', 'tests'])
- ->withPhpSets(php81: true)
+ ->withPhpSets(php82: true)
->withPreparedSets(deadCode: true, codingStyle: true, typeDeclarations: true)
->withSkip([
ReadOnlyPropertyRector::class,
ClosureToArrowFunctionRector::class,
FirstClassCallableRector::class,
NullToStrictStringFuncCallArgRector::class,
- BooleanInIfConditionRuleFixerRector::class,
- BooleanInBooleanNotRuleFixerRector::class,
RemoveUnusedPrivateMethodRector::class,
SimplifyUselessVariableRector::class,
NullableCompareToNullRector::class,
EncapsedStringsToSprintfRector::class,
- StaticClosureRector::class,
SplitDoubleAssignRector::class,
]);
diff --git a/src/Exceptions/UnknownApplicationConfiguration.php b/src/Exceptions/UnknownApplicationConfiguration.php
new file mode 100644
index 00000000..6f0cf899
--- /dev/null
+++ b/src/Exceptions/UnknownApplicationConfiguration.php
@@ -0,0 +1,9 @@
+> */
private array $model_classes = [];
- public function __construct(Filesystem $files, private SchemaAggregator $schema)
+ private SchemaAggregator $schema;
+
+ /**
+ * While the setter of a required property is an anti-pattern,
+ * this is the only way to be less independent of changes in the parent ModelsCommand constructor.
+ */
+ public function setSchemaAggregator(SchemaAggregator $schemaAggregator): void
{
- parent::__construct($files);
+ $this->schema = $schemaAggregator;
}
/** @return list> */
@@ -109,7 +115,6 @@ public function getPropertiesFromTable($model): void
}
if ($column->nullable) {
- /** @psalm-suppress MixedArrayAssignment */
$this->nullableColumns[$column_name] = true;
}
diff --git a/src/Handlers/Eloquent/ModelFactoryTypeProvider.php b/src/Handlers/Eloquent/ModelFactoryTypeProvider.php
new file mode 100644
index 00000000..a028d9e0
--- /dev/null
+++ b/src/Handlers/Eloquent/ModelFactoryTypeProvider.php
@@ -0,0 +1,61 @@
+> */
+ #[\Override]
+ public static function getClassLikeNames(): array
+ {
+ return ModelStubProvider::getModelClasses();
+ }
+
+ #[\Override]
+ public static function getPropertyType(PropertyTypeProviderEvent $event): ?Type\Union
+ {
+ if ($event->getPropertyName() !== 'factory') {
+ return null;
+ }
+
+ $source = $event->getSource();
+ if ($source === null) {
+ return null;
+ }
+
+ $classlike = $source->getCodebase()->classlike_storage_provider->get($event->getFqClasslikeName());
+
+ $usesHasFactory = isset($classlike->used_traits[strtolower(HasFactory::class)]);
+ if (! $usesHasFactory) {
+ return null;
+ }
+
+ $hasFactoryProperty = isset($classlike->properties['factory']);
+ // Check for static $factory property
+ if ($hasFactoryProperty && isset($classlike->properties['factory']->type)) {
+ $factoryType = $classlike->properties['factory']->type;
+ foreach ($factoryType->getAtomicTypes() as $type) {
+ if ($type instanceof Type\Atomic\TNamedObject) {
+ return new Type\Union([new Type\Atomic\TNamedObject($type->value)]);
+ }
+ }
+ }
+
+ // Default to Factory
+ return new Type\Union([
+ new Type\Atomic\TGenericObject('Illuminate\Database\Eloquent\Factories\Factory', [
+ new Type\Union([new Type\Atomic\TNamedObject($event->getFqClasslikeName())])
+ ])
+ ]);
+ }
+}
diff --git a/src/Handlers/Eloquent/ModelRelationshipPropertyHandler.php b/src/Handlers/Eloquent/ModelRelationshipPropertyHandler.php
index 594306b4..503a7ba9 100644
--- a/src/Handlers/Eloquent/ModelRelationshipPropertyHandler.php
+++ b/src/Handlers/Eloquent/ModelRelationshipPropertyHandler.php
@@ -4,11 +4,11 @@
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
-use PhpParser;
use Psalm\Codebase;
use Psalm\LaravelPlugin\Providers\ModelStubProvider;
use Psalm\Plugin\EventHandler\Event\PropertyExistenceProviderEvent;
@@ -23,6 +23,7 @@
use Psalm\Type\Union;
use function in_array;
+use function is_a;
class ModelRelationshipPropertyHandler implements
PropertyExistenceProviderInterface,
@@ -154,7 +155,25 @@ public static function getPropertyType(PropertyTypeProviderEvent $event): ?Union
private static function relationExists(Codebase $codebase, string $fq_classlike_name, string $property_name): bool
{
- // @todo: ensure this is a relation method
- return $codebase->methodExists($fq_classlike_name . '::' . $property_name);
+ $method = $fq_classlike_name . '::' . $property_name;
+
+ if (!$codebase->methodExists($method)) {
+ return false;
+ }
+
+ // ensure this is a relation method
+
+ $return_type = $codebase->getMethodReturnType($method, $fq_classlike_name);
+ if (!$return_type) {
+ return false;
+ }
+
+ foreach ($return_type->getAtomicTypes() as $type) {
+ if ($type instanceof TGenericObject && is_a($type->value, Relation::class, true)) {
+ return true;
+ }
+ }
+
+ return false;
}
}
diff --git a/src/Handlers/Eloquent/Schema/SchemaAggregator.php b/src/Handlers/Eloquent/Schema/SchemaAggregator.php
index bd8fa246..727dbcdf 100644
--- a/src/Handlers/Eloquent/Schema/SchemaAggregator.php
+++ b/src/Handlers/Eloquent/Schema/SchemaAggregator.php
@@ -57,7 +57,6 @@ public function addStatements(array $stmts): void
{
$nodeFinder = new NodeFinder();
- /** @var PhpParser\Node\Stmt\Class_[] $classes */
$classes = $nodeFinder->findInstanceOf($stmts, PhpParser\Node\Stmt\Class_::class);
foreach ($classes as $stmt) {
@@ -236,7 +235,7 @@ private function processColumnUpdates(string $table_name, string $call_arg_name,
*
* Process this ->nullable(false)
*/
- if (count($root_var->args) > 0) {
+ if ($root_var->args !== []) {
$first_argument_of_nullable = $root_var->args[0];
if (
$first_argument_of_nullable instanceof PhpParser\Node\Arg
diff --git a/src/Plugin.php b/src/Plugin.php
index 2da1512c..2cdd6764 100644
--- a/src/Plugin.php
+++ b/src/Plugin.php
@@ -10,6 +10,7 @@
use Psalm\LaravelPlugin\Handlers\Auth\RequestHandler;
use Psalm\LaravelPlugin\Handlers\Eloquent\ModelMethodHandler;
use Psalm\LaravelPlugin\Handlers\Eloquent\ModelPropertyAccessorHandler;
+use Psalm\LaravelPlugin\Handlers\Eloquent\ModelFactoryTypeProvider;
use Psalm\LaravelPlugin\Handlers\Eloquent\ModelRelationshipPropertyHandler;
use Psalm\LaravelPlugin\Handlers\Eloquent\RelationsMethodHandler;
use Psalm\LaravelPlugin\Handlers\Helpers\CacheHandler;
@@ -43,7 +44,12 @@ public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement
ApplicationProvider::bootApp();
$this->generateStubFiles();
} catch (\Throwable $throwable) {
- fwrite(\STDERR, "Laravel plugin error: “{$throwable->getMessage()}”\n");
+ $failOnInternalError = ((string) $config?->failOnInternalError) === 'true';
+ if ($failOnInternalError) {
+ throw $throwable;
+ }
+
+ fwrite(\STDERR, "\nLaravel plugin error on generating stub files: \"{$throwable->getMessage()}\"\n");
return;
}
@@ -73,9 +79,9 @@ protected function getCommonStubs(): array
/** @return list */
protected function getTaintAnalysisStubs(): array
{
- return array_merge(
- glob(dirname(__DIR__) . '/stubs/TaintAnalysis/Http/*.stubphp'),
- );
+ return [
+ ...glob(dirname(__DIR__) . '/stubs/common/TaintAnalysis/Http/*.stubphp') ?: []
+ ];
}
/** @return list */
@@ -83,10 +89,10 @@ protected function getStubsForVersion(string $version): array
{
[$majorVersion] = explode('.', $version);
- return array_merge(
- glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/*.stubphp'),
- glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/**/*.stubphp'),
- );
+ return [
+ ...glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/*.stubphp') ?: [],
+ ...glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/**/*.stubphp') ?: [],
+ ];
}
private function registerStubs(RegistrationInterface $registration): void
@@ -121,6 +127,8 @@ private function registerHandlers(RegistrationInterface $registration): void
require_once 'Handlers/Eloquent/ModelRelationshipPropertyHandler.php';
$registration->registerHooksFromClass(ModelRelationshipPropertyHandler::class);
+ require_once 'Handlers/Eloquent/ModelFactoryTypeProvider.php';
+ $registration->registerHooksFromClass(ModelFactoryTypeProvider::class);
require_once 'Handlers/Eloquent/ModelPropertyAccessorHandler.php';
$registration->registerHooksFromClass(ModelPropertyAccessorHandler::class);
require_once 'Handlers/Eloquent/RelationsMethodHandler.php';
diff --git a/src/Providers/ApplicationProvider.php b/src/Providers/ApplicationProvider.php
index dacb4f15..5b6d0435 100644
--- a/src/Providers/ApplicationProvider.php
+++ b/src/Providers/ApplicationProvider.php
@@ -7,6 +7,10 @@
use Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider;
use Illuminate\Container\Container;
use Illuminate\Contracts\Console\Kernel;
+use Illuminate\View\Factory;
+use Illuminate\View\FileViewFinder;
+use Illuminate\View\Engines\EngineResolver;
+use Illuminate\View\Engines\PhpEngine;
use Illuminate\Foundation\Application as LaravelApplication;
use Orchestra\Testbench\Concerns\CreatesApplication;
@@ -14,7 +18,6 @@
use function defined;
use function dirname;
use function file_exists;
-use function get_class;
use function getcwd;
use function microtime;
@@ -26,16 +29,11 @@ final class ApplicationProvider
public static function bootApp(): void
{
- $app = self::getApp();
-
- /** @var \Illuminate\Contracts\Console\Kernel $consoleApp */
- $consoleApp = $app->make(Kernel::class);
- // @todo do not bootstrap \Illuminate\Foundation\Bootstrap\HandleExceptions
- $consoleApp->bootstrap();
-
- $app->register(IdeHelperServiceProvider::class);
+ self::getApp();
}
+ private static bool $booted = false;
+
public static function getApp(): LaravelApplication
{
if (self::$app instanceof Container) {
@@ -46,14 +44,14 @@ public static function getApp(): LaravelApplication
define('LARAVEL_START', microtime(true));
}
- if (file_exists($applicationPath = getcwd() . '/bootstrap/app.php')) { // Applications and Local Dev
+ if (file_exists($applicationPath = (getcwd() ?: '.') . '/bootstrap/app.php')) { // Applications and Local Dev
/** @psalm-suppress MixedAssignment */
$app = require $applicationPath;
} elseif (file_exists($applicationPath = dirname(__DIR__, 5) . '/bootstrap/app.php')) { // plugin installed to vendor
/** @psalm-suppress MixedAssignment */
$app = require $applicationPath;
} else { // Laravel Packages
- $app = (new self())->createApplication(); // Orchestra\Testbench
+ $app = (new self())->createApplication(); // Orchestra\Testbench (e.g., test:type command)
}
if (! $app instanceof LaravelApplication) {
@@ -62,6 +60,31 @@ public static function getApp(): LaravelApplication
self::$app = $app;
+ // Initialize view system first
+ if (!$app->bound('view')) {
+ $filesystem = new \Illuminate\Filesystem\Filesystem();
+ $viewFinder = new FileViewFinder($filesystem, []);
+ $engineResolver = new EngineResolver();
+ $engineResolver->register('php', fn() => new PhpEngine($filesystem));
+ $app->singleton('view', fn() => new Factory($engineResolver, $viewFinder, $app['events']));
+ }
+
+ if (!self::$booted) {
+ // Bootstrap console app
+ $consoleApp = $app->make(Kernel::class);
+ $app->bind('Illuminate\Foundation\Bootstrap\HandleExceptions', function () {
+ return new class {
+ public function bootstrap(): void
+ {
+ }
+ };
+ });
+ $consoleApp->bootstrap();
+
+ $app->register(IdeHelperServiceProvider::class);
+ self::$booted = true;
+ }
+
return $app;
}
@@ -82,24 +105,18 @@ protected function resolveApplicationBootstrappers(LaravelApplication $app): voi
// we want to keep the default psalm exception handler, otherwise the Laravel one will always return exit codes
// of 0
//$app->make('Illuminate\Foundation\Bootstrap\HandleExceptions')->bootstrap($app);
- /** @psalm-suppress MixedMethodCall */
$app->make(\Illuminate\Foundation\Bootstrap\RegisterFacades::class)->bootstrap($app);
- /** @psalm-suppress MixedMethodCall */
$app->make(\Illuminate\Foundation\Bootstrap\SetRequestForConsole::class)->bootstrap($app);
- /** @psalm-suppress MixedMethodCall */
$app->make(\Illuminate\Foundation\Bootstrap\RegisterProviders::class)->bootstrap($app);
$this->getEnvironmentSetUp($app);
- /** @psalm-suppress MixedMethodCall */
$app->make(\Illuminate\Foundation\Bootstrap\BootProviders::class)->bootstrap($app);
foreach ($this->getPackageBootstrappers($app) as $bootstrap) {
- /** @psalm-suppress MixedMethodCall */
$app->make($bootstrap)->bootstrap($app);
}
- /** @psalm-suppress MixedMethodCall */
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
/** @var \Illuminate\Routing\Router $router */
@@ -127,5 +144,23 @@ protected function getEnvironmentSetUp(LaravelApplication $app): void
$config->set('ide-helper.model_locations', [
'../../../../tests/Application/app/Models',
]);
+
+ // Set up view paths for ide-helper
+ $viewPath = dirname((new \ReflectionClass(IdeHelperServiceProvider::class))->getFileName(), 2) . '/resources/views';
+
+ if (!$app->bound('view')) {
+ $filesystem = new \Illuminate\Filesystem\Filesystem();
+
+ // Set up the view finder
+ $viewFinder = new FileViewFinder($filesystem, [$viewPath]);
+
+ // Set up the engine resolver
+ $engineResolver = new EngineResolver();
+ $engineResolver->register('php', fn() => new PhpEngine($filesystem));
+
+ // Create and bind the view factory
+ $app->singleton('view', fn() => new Factory($engineResolver, $viewFinder, $app['events']));
+ }
+ $app['view']->addNamespace('ide-helper', $viewPath);
}
}
diff --git a/src/Providers/ConfigRepositoryProvider.php b/src/Providers/ConfigRepositoryProvider.php
index b3939401..b7afe9a4 100644
--- a/src/Providers/ConfigRepositoryProvider.php
+++ b/src/Providers/ConfigRepositoryProvider.php
@@ -8,10 +8,8 @@
final class ConfigRepositoryProvider
{
- /** @psalm-suppress MixedInferredReturnType */
public static function get(): Repository
{
- /** @psalm-suppress MixedReturnStatement */
return ApplicationProvider::getApp()->get(Repository::class);
}
}
diff --git a/src/Providers/FacadeStubProvider.php b/src/Providers/FacadeStubProvider.php
index d6773fa7..e40d946e 100644
--- a/src/Providers/FacadeStubProvider.php
+++ b/src/Providers/FacadeStubProvider.php
@@ -31,10 +31,15 @@ public static function generateStubFile(): void
$fake_filesystem = new FakeFilesystem();
+ /**
+ * @var \Illuminate\View\Factory $viewFactory
+ */
+ $viewFactory = $app->make('view');
+
$stubs_generator_command = new GeneratorCommand(
$config,
$fake_filesystem,
- ViewFactoryProvider::get(),
+ $viewFactory
);
$stubs_generator_command->setLaravel($app);
diff --git a/src/Providers/ModelStubProvider.php b/src/Providers/ModelStubProvider.php
index 326dbc19..c0ecebf7 100644
--- a/src/Providers/ModelStubProvider.php
+++ b/src/Providers/ModelStubProvider.php
@@ -3,6 +3,7 @@
namespace Psalm\LaravelPlugin\Providers;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
+use Psalm\LaravelPlugin\Exceptions\UnknownApplicationConfiguration;
use Psalm\LaravelPlugin\Fakes\FakeFilesystem;
use Psalm\LaravelPlugin\Fakes\FakeModelsCommand;
use Psalm\LaravelPlugin\Handlers\Eloquent\Schema\SchemaAggregator;
@@ -12,6 +13,7 @@
use function glob;
use function method_exists;
use function unlink;
+use function is_array;
final class ModelStubProvider implements GeneratesStubs
{
@@ -33,7 +35,12 @@ public static function generateStubFile(): void
$schema_aggregator = new SchemaAggregator();
- foreach (glob($migrations_directory . '*.php') as $file) {
+ $migrationFilePathnames = glob($migrations_directory . '*.php');
+ if (! is_array($migrationFilePathnames)) {
+ throw new UnknownApplicationConfiguration("No migration files found in {$migrations_directory} directory.");
+ }
+
+ foreach ($migrationFilePathnames as $file) {
$schema_aggregator->addStatements($codebase->getStatementsForFile($file));
}
@@ -41,9 +48,10 @@ public static function generateStubFile(): void
$models_generator_command = new FakeModelsCommand(
$fake_filesystem,
- $schema_aggregator
+ $app->make(\Illuminate\Contracts\Config\Repository::class),
+ $app->make(\Illuminate\View\Factory::class)
);
-
+ $models_generator_command->setSchemaAggregator($schema_aggregator);
$models_generator_command->setLaravel($app);
@unlink(self::getStubFileLocation());
diff --git a/src/Providers/ViewFactoryProvider.php b/src/Providers/ViewFactoryProvider.php
deleted file mode 100644
index 8b3d1f60..00000000
--- a/src/Providers/ViewFactoryProvider.php
+++ /dev/null
@@ -1,39 +0,0 @@
-getFileName();
-
- if ($file_path === false) {
- throw new UnexpectedValueException('Service helper should have a file path');
- }
-
- $resolver = new EngineResolver();
- $fake_filesystem = new FakeFilesystem();
- $resolver->register('php', function () use ($fake_filesystem): PhpEngine {
- return new PhpEngine($fake_filesystem);
- });
- $finder = new FileViewFinder($fake_filesystem, [dirname($file_path) . '/../resources/views']);
- $factory = new Factory($resolver, $finder, new Dispatcher());
- $factory->addExtension('php', 'php');
- return $factory;
- }
-}
diff --git a/stubs/common/Database/Migrations/Migrator.stubphp b/stubs/common/Database/Migrations/Migrator.stubphp
index a1580a7b..28da5fee 100644
--- a/stubs/common/Database/Migrations/Migrator.stubphp
+++ b/stubs/common/Database/Migrations/Migrator.stubphp
@@ -7,10 +7,10 @@ class Migrator
/**
* Execute the given callback using the given connection as the default connection.
*
- * @template TCalbackReturn
+ * @template TCallbackReturn
* @param string $name
- * @param callable(): TCalbackReturn $callback
- * @return TCalbackReturn
+ * @param callable(): TCallbackReturn $callback
+ * @return TCallbackReturn
*/
public function usingConnection($name, callable $callback) {}
}
diff --git a/tests/Application/README.md b/tests/Application/README.md
index da013e48..8573148c 100644
--- a/tests/Application/README.md
+++ b/tests/Application/README.md
@@ -4,6 +4,6 @@ Idea of application test: create an [almost] empty Laravel app and run Psalm ove
## FAQ
- - Q1: How to update baselines
- - A1: Inside .sh files change `--use-baseline` to `--set-baseline`, run sh files and revert changes in .sh files.
+ - Q1: How to update the baseline file for the Laravel Application test?
+ - A1: Run `laravel-test.sh` script with the `-u` (or `--update`) flag, e.g., `./laravel-test.sh -u`.
____
diff --git a/tests/Application/laravel-test-baseline.xml b/tests/Application/laravel-test-baseline.xml
index 355f442c..24a684d8 100644
--- a/tests/Application/laravel-test-baseline.xml
+++ b/tests/Application/laravel-test-baseline.xml
@@ -1,136 +1,171 @@
-
+
- array|bool
+
- ExampleChannel
+
- CastsAttributes
+
-
- $attributes
- $attributes
-
- ExampleCast
+
- handle
+
+
+
+
+
+
+
+
+
+
+
- ExampleCommand
+
+
+
+
+
+
- ExampleException
+
-
-
- $e
-
-
- ExampleController
+
- ExampleMiddleware
+
-
-
- $except
-
-
-
- ]]>
-
+
+
+
+
+
+
+
+
+
+
+
- ExampleRequest
+
]]>
- parent::toArray($request)
+
- ExampleResource
+
+
+
+
+
+
- ExampleListener
+
- attachments
- content
- envelope
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- ExampleScope
+
- $casts
- $fillable
- $hidden
+
+
+
+
+
+
+
+
+
+
+
+
+
- ExampleObserver
+
- ExamplePolicy
+
-
-
- $listen
-
+
+
+
+
- ExampleProvider
+
-
-
- user()?->id]]>
-
-
- ExampleRule
+
- ExampleComponent
- ExampleComponent
+
+
- ExampleComponent
+
diff --git a/tests/Application/laravel-test-psalm.xml b/tests/Application/laravel-test-psalm.xml
index 9c03cfce..d9fd584c 100644
--- a/tests/Application/laravel-test-psalm.xml
+++ b/tests/Application/laravel-test-psalm.xml
@@ -5,9 +5,9 @@
findUnusedBaselineEntry="false"
findUnusedCode="true"
resolveFromConfigFile="false"
- xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
+ xsi:schemaLocation="https://getpsalm.org/schema/config ../../vendor/vimeo/psalm/config.xsd"
errorBaseline="../../tests/Application/laravel-test-baseline.xml"
- phpVersion="8.0"
+ phpVersion="8.2"
>
@@ -16,11 +16,14 @@
-
+
+ true
+
+
diff --git a/tests/Application/laravel-test.sh b/tests/Application/laravel-test.sh
index b7e51137..b1d805e3 100755
--- a/tests/Application/laravel-test.sh
+++ b/tests/Application/laravel-test.sh
@@ -1,19 +1,117 @@
-#!/bin/bash
+#!/usr/bin/env bash
+# Laravel Test Environment Setup Script
+# This script sets up a fresh Laravel installation and runs Psalm analysis
+
+# Exit on error. Append "|| true" if you expect an error.
set -e
+# Exit on error in any nested commands
+set -o pipefail
+# Catch the error in case a variable is not set
+set -u
+
+# See https://github.com/laravel/laravel/tags for Laravel versions
+LARAVEL_INSTALLER_VERSION=11.6
+
+# Terminal colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Default values
+UPDATE_BASELINE=false
+VERBOSE=false
+REMOVE=false
+
+# Function to display script usage
+show_help() {
+ cat << EOF
+Usage: $(basename "$0") [options]
+
+Sets up a fresh Laravel installation and runs Psalm analysis.
+
+Options:
+ -h, --help Show this help message
+ -u, --update Update Psalm baseline
+ -v, --verbose Enable verbose output
+ -r, --remove Remove Laravel project directory after execution
+
+Environment variables:
+ COMPOSER_MEMORY_LIMIT Memory limit for Composer (default: -1)
+EOF
+}
+# Function to display error messages
+error() {
+ echo -e "${RED}Error: $1${NC}" >&2
+ exit 1
+}
+
+# Function to display info messages
+info() {
+ echo -e "${GREEN}$1${NC}"
+}
+
+# Function to display debug messages
+debug() {
+ if [ "$VERBOSE" = true ]; then
+ echo -e "${YELLOW}Debug: $1${NC}"
+ fi
+}
+
+# Cleanup function
+cleanup() {
+ if [ -d "$APP_INSTALLATION_PATH" ]; then
+ info "Removing the installation directory..."
+ rm -rf "$APP_INSTALLATION_PATH"
+ fi
+}
+
+# Parse command line arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -h|--help)
+ show_help
+ exit 0
+ ;;
+ -u|--update)
+ UPDATE_BASELINE=true
+ shift
+ ;;
+ -r|--remove)
+ REMOVE=true
+ shift
+ ;;
+ -v|--verbose)
+ VERBOSE=true
+ shift
+ ;;
+ *)
+ error "Unknown option: $1\nUse --help for usage information."
+ ;;
+ esac
+done
+
+if [ "$REMOVE" = true ]; then
+ # Set up trap to clean up on script exit
+ trap cleanup EXIT
+fi
+
+# Get absolute path of script directory
CURRENT_SCRIPT_PATH="$( cd "$(dirname "$0")" ; pwd -P )"
APP_INSTALLATION_PATH="$(dirname "$(dirname "$CURRENT_SCRIPT_PATH")")/tests-app/laravel-example"
-echo "Cleaning up previous installation"
-rm -rf $APP_INSTALLATION_PATH
+if [ -d "$APP_INSTALLATION_PATH" ]; then
+ info "Removing previous installation"
+ rm -rf "$APP_INSTALLATION_PATH"
+fi
-echo "Installing Laravel"
-# See https://github.com/laravel/laravel/tags for Laravel versions
-composer create-project --quiet --prefer-dist laravel/laravel $APP_INSTALLATION_PATH 10.0 --quiet --prefer-dist
-cd $APP_INSTALLATION_PATH
+info "Creating a new Laravel project (installer v${LARAVEL_INSTALLER_VERSION})"
+composer create-project --quiet --prefer-dist laravel/laravel "$APP_INSTALLATION_PATH" $LARAVEL_INSTALLER_VERSION
+cd "$APP_INSTALLATION_PATH"
-echo "Preparing Laravel"
+info "Making different types of classes for Laravel"
./artisan make:cast ExampleCast
./artisan make:channel ExampleChannel
./artisan make:component ExampleComponent
@@ -37,12 +135,26 @@ echo "Preparing Laravel"
./artisan make:scope ExampleScope
./artisan make:seeder ExampleSeeder
-echo "Adding package from source"
+info "Adding package from source"
composer config repositories.0 '{"type": "path", "url": "../../"}'
composer config minimum-stability 'dev'
COMPOSER_MEMORY_LIMIT=-1 composer require --dev "psalm/plugin-laravel:*" --update-with-all-dependencies
-echo "Analyzing Laravel"
-./vendor/bin/psalm -c ../../tests/Application/laravel-test-psalm.xml --use-baseline=../../tests/Application/laravel-test-baseline.xml
+info "Analyzing Laravel"
+PSALM_CONFIG="../../tests/Application/laravel-test-psalm.xml"
+PSALM_BASELINE="../../tests/Application/laravel-test-baseline.xml"
+
+if [ "$UPDATE_BASELINE" = true ]; then
+ info "Updating Psalm baseline"
+ ./vendor/bin/psalm --config="$PSALM_CONFIG" --set-baseline="$PSALM_BASELINE"
+ info "Baseline file $PSALM_BASELINE is updated, please check the changes and commit them."
+else
+ info "Running Psalm analysis"
+ ./vendor/bin/psalm --config="$PSALM_CONFIG" --use-baseline="$PSALM_BASELINE"
+fi
+
+echo
-echo -e "\nA sample Laravel application installed at the $APP_INSTALLATION_PATH directory, feel free to remove it."
+if [ "$REMOVE" = false ]; then
+ info "A sample Laravel application installed at the $APP_INSTALLATION_PATH directory, feel free to remove it."
+fi
diff --git a/tests/README.md b/tests/README.md
index 219055f9..6c9f85c7 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,6 +1,6 @@
# Tests
There are 3 types of tests:
-1. Type (main one): Use phpt files to run Psalm over code snippets in the context of fake Laravel app [using orchestra/testbench]
-2. Application: create an [almost] empty Laravel app and run Psalm over its codebase.
-3. Unit: use PHPUnit to test some internal logic without running Psalm.
+1. **Type** (the main one): uses .phpt files to run Psalm over code snippets in the context of a fake Laravel app [using orchestra/testbench]
+2. **Application**: creates an empty Laravel app, adds some typical classes of different types and run Psalm over its codebase.
+3. **Unit**: uses PHPUnit to test some internal logic without running Psalm.
diff --git a/tests/Type/PsalmTest.php b/tests/Type/PsalmTest.php
index 5060bf8d..5f3d2652 100644
--- a/tests/Type/PsalmTest.php
+++ b/tests/Type/PsalmTest.php
@@ -14,8 +14,8 @@ final class PsalmTest extends TestCase
{
private ?PsalmTester $psalmTester = null;
- /** @return iterable */
- public static function providePhptFiles(): iterable
+ /** @return \Generator */
+ public static function providePhptFiles(): \Generator
{
$baseDir = __DIR__ . \DIRECTORY_SEPARATOR . 'tests' . \DIRECTORY_SEPARATOR;
$testExtension = 'phpt';
@@ -24,14 +24,11 @@ public static function providePhptFiles(): iterable
$itr = new \RecursiveIteratorIterator($dirItr);
$regItr = new \RegexIterator($itr, "/^.+.{$testExtension}\$/", \RegexIterator::GET_MATCH);
- $filePaths = [];
-
foreach ($regItr as $file) {
$filepath = $file[0];
- $filePaths[str_replace($baseDir, '', $filepath)] = [$filepath];
+ $relativeFilepath = str_replace($baseDir, '', $filepath);
+ yield $relativeFilepath => [$filepath];
}
-
- return $filePaths;
}
#[DataProvider('providePhptFiles')]
diff --git a/tests/Type/psalm.xml b/tests/Type/psalm.xml
index af276794..ca05f200 100644
--- a/tests/Type/psalm.xml
+++ b/tests/Type/psalm.xml
@@ -11,9 +11,11 @@
sealAllMethods="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
- xsi:schemaLocation="https://getpsalm.org/schema/config ../vendor/vimeo/psalm/config.xsd"
+ xsi:schemaLocation="https://getpsalm.org/schema/config ../../vendor/vimeo/psalm/config.xsd"
>
-
+
+ true
+
diff --git a/tests/Type/tests/ContainerTest.phpt b/tests/Type/tests/ContainerTest.phpt
index 3556126d..6f252b9e 100644
--- a/tests/Type/tests/ContainerTest.phpt
+++ b/tests/Type/tests/ContainerTest.phpt
@@ -64,5 +64,4 @@ function cannotResolveUnknownDependency(): \Illuminate\Log\LogManager
?>
--EXPECTF--
UndefinedMagicMethod on line %d: Magic method Illuminate\Foundation\Application::undefined_method does not exist
-MixedInferredReturnType on line %d: Could not verify return type 'Illuminate\Log\LogManager' for cannotResolveUnknownDependency
MixedReturnStatement on line %d: Could not infer a return type