diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99f64570..e1c46cd3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,40 +8,11 @@ jobs: strategy: fail-fast: false matrix: - php: [7.3, 7.4, 8.0, 8.1] - illuminate_version: [6.*, 8.*] + php: [8.0, 8.1] + illuminate_version: [8.67.*, 9.7.*] stability: [prefer-lowest, prefer-stable] - exclude: - - illuminate_version: 6.* - php: 7.3 - stability: prefer-stable - - illuminate_version: 6.* - php: 7.4 - stability: prefer-stable - - - illuminate_version: 6.* - php: 8.0 - stability: prefer-stable - - # Exclude unsupported combination - # https://laravel.com/docs/8.x/releases#support-policy - - illuminate_version: 6.* - php: 8.1 - - # "Added PHP 8.1 Support from v8.67.0" - # https://github.com/laravel/framework/blob/8.x/CHANGELOG-8.x.md#v8670-2021-10-22 - # see also `matrix.include` section - - illuminate_version: 8.* - php: 8.1 - - include: - # "Added PHP 8.1 Support from v8.67.0" - - illuminate_version: ^8.67.0 - php: 8.1 - stability: 'prefer-stable' - - name: ${{ matrix.php }} | Illuminate ${{ matrix.illuminate_version }} | ${{ matrix.stability }} + name: P${{ matrix.php }} | I ${{ matrix.illuminate_version }} | ${{ matrix.stability }} steps: - name: Checkout code @@ -56,7 +27,7 @@ jobs: - name: Install dependencies run: | - composer require "illuminate/container:${{ matrix.illuminate_version }}" "illuminate/contracts:${{ matrix.illuminate_version }}" "illuminate/database:${{ matrix.illuminate_version }}" "illuminate/http:${{ matrix.illuminate_version }}" "illuminate/support:${{ matrix.illuminate_version }}" --no-interaction --no-progress --prefer-dist --${{ matrix.stability }} + composer require "laravel/framework:${{ matrix.illuminate_version }}" "illuminate/container:${{ matrix.illuminate_version }}" "illuminate/contracts:${{ matrix.illuminate_version }}" "illuminate/database:${{ matrix.illuminate_version }}" "illuminate/http:${{ matrix.illuminate_version }}" "illuminate/support:${{ matrix.illuminate_version }}" --no-interaction --no-progress --prefer-dist --${{ matrix.stability }} - name: Run Tests run: composer test diff --git a/composer.json b/composer.json index 86153f33..c5ddb2aa 100644 --- a/composer.json +++ b/composer.json @@ -11,28 +11,32 @@ } ], "require": { - "php": "^7.3|^8.0", + "php": "^8.0", "ext-simplexml": "*", - "illuminate/config": "^6.0 || ^8.0", - "illuminate/container": "^6.0 || ^8.0", - "illuminate/contracts": "^6.0 || ^8.0", - "illuminate/database": "^6.0 || ^8.0", - "illuminate/events": "^6.0 || ^8.0", - "illuminate/http": "^6.0 || ^8.0", - "illuminate/routing": "^6.0 || ^8.0", - "illuminate/support": "^6.0 || ^8.0", - "illuminate/view": "^6.0 || ^8.0", + "illuminate/config": "^8.0 || ^9.0", + "illuminate/container": "^8.0 || ^9.0", + "illuminate/contracts": "^8.0 || ^9.0", + "illuminate/database": "^8.0 || ^9.0", + "illuminate/events": "^8.0 || ^9.0", + "illuminate/http": "^8.0 || ^9.0", + "illuminate/routing": "^8.0 || ^9.0", + "illuminate/support": "^8.0 || ^9.0", + "illuminate/view": "^8.0 || ^9.0", "vimeo/psalm": "^4.8.1", - "orchestra/testbench": "^3.8 || ^4.0 || ^5.0 || ^6.22 || ^7.0", - "barryvdh/laravel-ide-helper": ">=2.8.0" + "orchestra/testbench": "^6.22 || ^7.0", + "barryvdh/laravel-ide-helper": "^2.10" }, "require-dev": { - "codeception/codeception": "^4.1.6", - "codeception/module-phpbrowser": "^1.0.0", - "codeception/module-asserts": "^1.0.0", - "weirdan/codeception-psalm-module": "^0.13.1", + "codeception/codeception": "^5.0", + "codeception/module-asserts": "*@dev", + "codeception/module-cli": "^2.0", + "codeception/module-filesystem": "^3.0", + "codeception/module-phpbrowser": "*@dev", + "slevomat/coding-standard": "^6.2", "squizlabs/php_codesniffer": "*", - "slevomat/coding-standard": "^6.2" + "phpoption/phpoption": "^1.8.0", + "symfony/http-foundation": "^5.3.7 || ^6.0", + "ramsey/collection": "^1.2.0" }, "autoload": { "psr-4": { diff --git a/phpcs.xml b/phpcs.xml index daaa4f57..c388a1fe 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -22,4 +22,5 @@ tests src/cache/ tests/_support/ + tests/Support/ diff --git a/psalm-baseline.xml b/psalm-baseline.xml index d2d7f5ee..63b50bc1 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,10 @@ - + + + + require $applicationPath + + databasePath diff --git a/src/Fakes/FakeMetaCommand.php b/src/Fakes/FakeMetaCommand.php index d15e49e9..98fc7ef3 100644 --- a/src/Fakes/FakeMetaCommand.php +++ b/src/Fakes/FakeMetaCommand.php @@ -14,7 +14,6 @@ protected function registerClassAutoloadExceptions(): callable // by default, the ide-helper throws exceptions when it cannot find a class. However it does not unregister that // autoloader when it is done, and we certainly do not want to throw exceptions when we are simply checking if // a certain class exists. We are instead changing this to be a noop. - return function () { }; } diff --git a/src/Fakes/FakeModelsCommandLogic.php b/src/Fakes/FakeModelsCommandLogic.php index 6674d796..bf05b2bb 100644 --- a/src/Fakes/FakeModelsCommandLogic.php +++ b/src/Fakes/FakeModelsCommandLogic.php @@ -26,7 +26,7 @@ public function getModels(): array * * @param Model $model */ - protected function getPropertiesFromTable($model): void + public function getPropertiesFromTable($model): void { $table_name = $model->getTable(); diff --git a/stubs/EloquentBuilder.stubphp b/stubs/EloquentBuilder.stubphp index 37799233..ad027291 100644 --- a/stubs/EloquentBuilder.stubphp +++ b/stubs/EloquentBuilder.stubphp @@ -167,6 +167,20 @@ class Builder */ public function firstOr($columns = ['*'], Closure $callback = null) { } + /** + * @param array $attributes + * @param array $values + * @return TModel|self + */ + public function firstOrNew(array $attributes = [], array $values = []) { } + + /** + * @param array $attributes + * @param array $values + * @return TModel|self + */ + public function firstOrCreate(array $attributes = [], array $values = []) { } + /** * @param string $column * @return mixed diff --git a/stubs/HasOneOrMany.stubphp b/stubs/HasOneOrMany.stubphp index 13463c13..86414005 100644 --- a/stubs/HasOneOrMany.stubphp +++ b/stubs/HasOneOrMany.stubphp @@ -66,4 +66,20 @@ abstract class HasOneOrMany extends Relation * @psalm-return \Illuminate\Database\Eloquent\Collection */ public function createMany(array $records) { } + + /** + * @param array $attributes + * @param array $values + * @return \Illuminate\Database\Eloquent\Model + * @psalm-return TRelatedModel + */ + public function firstOrNew(array $attributes = [], array $values = []) { } + + /** + * @param array $attributes + * @param array $values + * @return \Illuminate\Database\Eloquent\Model + * @psalm-return TRelatedModel + */ + public function firstOrCreate(array $attributes = [], array $values = []) { } } diff --git a/tests/Support/Module.php b/tests/Support/Module.php new file mode 100644 index 00000000..8ceb2d45 --- /dev/null +++ b/tests/Support/Module.php @@ -0,0 +1,555 @@ + */ + private const VERSION_OPERATORS = [ + 'newer than' => '>', + 'older than' => '<', + ]; + + private const DEFAULT_PSALM_CONFIG = "\n" + . "\n" + . " \n" + . " \n" + . " \n" + . "\n"; + + /** + * @var ?Cli + */ + private $cli; + + /** + * @var ?Filesystem + */ + private $fs; + + /** + * @var array + * @psalm-suppress NonInvariantDocblockPropertyType + */ + protected array $config = [ + 'psalm_path' => 'vendor/bin/psalm', + 'default_dir' => 'tests/_run/', + ]; + + /** @var string */ + private $psalmConfig = ''; + + /** @var string */ + private $preamble = ''; + + /** @var ?array */ + private $errors = null; + + /** @var bool */ + private $hasAutoload = false; + + /** @var ?int */ + private $exitCode = null; + + /** @var ?string */ + protected $output = null; + + public function _beforeSuite($settings = []): void + { + $defaultDir = $this->config['default_dir']; + if (file_exists($defaultDir)) { + if (is_dir($defaultDir)) { + return; + } + unlink($defaultDir); + } + + if (!mkdir($defaultDir, 0755, true)) { + throw new TestRuntimeException('Failed to create dir: ' . $defaultDir); + } + } + + public function _before(TestInterface $test): void + { + $this->hasAutoload = false; + $this->errors = null; + $this->output = null; + $this->exitCode = null; + $this->config['psalm_path'] = realpath($this->config['psalm_path']); + $this->psalmConfig = ''; + $this->fs()->cleanDir($this->config['default_dir']); + $this->preamble = ''; + } + + /** + * @param string[] $options + */ + public function runPsalmOn(string $filename, array $options = []): void + { + $suppressProgress = $this->packageSatisfiesVersionConstraint('vimeo/psalm', '>=3.4.0'); + + $options = array_map('escapeshellarg', $options); + $cmd = $this->config['psalm_path'] + . ' --output-format=json ' + . ($suppressProgress ? ' --no-progress ' : ' ') + . join(' ', $options) . ' ' + . ($filename ? escapeshellarg($filename) : '') + . ' 2>&1'; + $this->debug('Running: ' . $cmd); + $this->cli()->runShellCommand($cmd, false); + + /** @psalm-suppress MissingPropertyType shouldn't be required, but older Psalm needs it */ + $this->output = (string)$this->cli()->output; + /** @psalm-suppress MissingPropertyType shouldn't be required, but older Psalm needs it */ + $this->exitCode = (int)$this->cli()->result; + + $this->debug(sprintf('Psalm exit code: %d', $this->exitCode)); + // $this->debug('Psalm output: ' . $this->output); + } + + /** + * @param string[] $options + */ + public function runPsalmIn(string $dir, array $options = []): void + { + $pwd = getcwd(); + $this->fs()->amInPath($dir); + + $config = $this->psalmConfig ?: self::DEFAULT_PSALM_CONFIG; + $config = sprintf($config, $this->hasAutoload ? 'autoloader="autoload.php"' : ''); + + $this->fs()->writeToFile('psalm.xml', $config); + + $this->runPsalmOn('', $options); + $this->fs()->amInPath($pwd); + } + + /** + * @Then I see exit code :code + */ + public function seeExitCode(string $exitCode): void + { + if ($this->exitCode === (int) $exitCode) { + return; + } + + Assert::fail("Expected exit code {$exitCode}, got {$this->exitCode}"); + } + + public function seeThisError(string $type, string $message): void + { + $this->parseErrors(); + if (empty($this->errors)) { + Assert::fail("No errors"); + } + + foreach ($this->errors as $i => $error) { + if ( + $this->matches($type, $error['type']) + && $this->matches($message, $error['message']) + ) { + unset($this->errors[$i]); + return; + } + } + + Assert::fail("Didn't see [ $type $message ] in: \n" . $this->remainingErrors()); + } + + private function matches(string $expected, string $actual): bool + { + $regexpDelimiter = '/'; + if ($expected[0] === $regexpDelimiter && $expected[strlen($expected) - 1] === $regexpDelimiter) { + $regexp = $expected; + } else { + $regexp = $this->convertToRegexp($expected); + } + + return (bool) preg_match($regexp, $actual); + } + + /** + * @Then I see no errors + * @Then I see no other errors + */ + public function seeNoErrors(): void + { + $this->parseErrors(); + if (!empty($this->errors)) { + Assert::fail("There were errors: \n" . $this->remainingErrors()); + } + } + + private function packageSatisfiesVersionConstraint(string $package, string $versionConstraint): bool + { + try { + $currentVersion = $this->getShortVersion($package); + } catch (OutOfBoundsException $ex) { + $this->debug(sprintf("Package %s is not installed", $package)); + return false; + } + + $this->debug(sprintf("Current version of %s : %s", $package, $currentVersion)); + + // todo: move to init/construct/before? + $parser = new VersionParser(); + $currentVersion = $parser->normalize($currentVersion); + + // restore pre-composer/semver:2.0 behaviour for comparison purposes + if (preg_match('/^dev-/', $currentVersion)) { + $currentVersion = '9999999-dev'; + } + + $result = Semver::satisfies($currentVersion, $versionConstraint); + + $this->debug("Comparing $currentVersion against $versionConstraint => " . ($result ? 'ok' : 'ko')); + + return $result; + } + + /** + * @deprecated + * This method is only to maintain the public API; please use `self::haveADependencySatisfied` instead. + */ + public function seePsalmVersionIs(string $operator, string $version): bool + { + return $this->packageSatisfiesVersionConstraint('vimeo/psalm', $operator . $version); + } + + /** + * @Given I have the following code preamble :code + */ + public function haveTheFollowingCodePreamble(PyStringNode $code): void + { + $this->preamble = $code->getRaw(); + } + + /** + * @When I run psalm + * @When I run Psalm + */ + public function runPsalm(): void + { + $this->runPsalmIn($this->config['default_dir']); + } + + /** + * @When I run Psalm with dead code detection + * @When I run psalm with dead code detection + */ + public function runPsalmWithDeadCodeDetection(): void + { + $this->runPsalmIn($this->config['default_dir'], ['--find-dead-code']); + } + + public function seePsalmHasTaintAnalysis(): bool + { + $taintAnalysisAvailable = $this->packageSatisfiesVersionConstraint('vimeo/psalm', '>=3.10.0'); + return $taintAnalysisAvailable; + } + + /** + * @Given I have Psalm with taint analysis + * @Given I have psalm with taint analysis + */ + public function havePsalmWithTaintAnalysis(): void + { + if (!$this->seePsalmHasTaintAnalysis()) { + /** @psalm-suppress InternalClass,InternalMethod */ + throw new SkippedTestError("This scenario requires Psalm with taint analysis (3.10+)"); + } + } + + /** + * @When I run Psalm with taint analysis + * @When I run psalm with taint analysis + */ + public function runPsalmWithTaintAnalysis(): void + { + if (!$this->seePsalmHasTaintAnalysis()) { + Assert::fail('Taint analysis is available since 3.10.0'); + } + $this->runPsalmIn($this->config['default_dir'], ['--track-tainted-input']); + } + + /** + * @When I run Psalm on :arg1 + * @When I run psalm on :arg1 + */ + public function runPsalmOnASingleFile(string $file): void + { + $pwd = getcwd(); + $this->fs()->amInPath($this->config['default_dir']); + + $config = $this->psalmConfig ?: self::DEFAULT_PSALM_CONFIG; + $config = sprintf($config, $this->hasAutoload ? 'autoloader="autoload.php"' : ''); + + $this->fs()->writeToFile('psalm.xml', $config); + + $this->runPsalmOn($file); + $this->fs()->amInPath($pwd); + } + + + /** + * @Given I have the following config :config + */ + public function haveTheFollowingConfig(PyStringNode $config): void + { + $this->psalmConfig = $config->getRaw(); + } + + /** + * @Given I have the following code :code + */ + public function haveTheFollowingCode(PyStringNode $code): void + { + $file = sprintf( + '%s/%s.php', + rtrim($this->config['default_dir'], '/'), + sha1($this->preamble . $code->getRaw()) + ); + + $this->fs()->writeToFile( + $file, + $this->preamble . $code + ); + } + + /** + * @Given I have some future Psalm that supports this feature :ref + */ + public function haveSomeFuturePsalmThatSupportsThisFeature(string $ref): void + { + /** @psalm-suppress InternalClass,InternalMethod */ + throw new SkippedTestError("Future functionality that Psalm has yet to support: $ref"); + } + + /** + * @Given /I have Psalm (newer than|older than) "([0-9.]+)" \(because of "([^"]+)"\)/ + */ + public function havePsalmOfACertainVersionRangeBecauseOf(string $operator, string $version, string $reason): void + { + if (!isset(self::VERSION_OPERATORS[$operator])) { + throw new TestRuntimeException("Unknown operator: $operator"); + } + + /** + * @psalm-suppress RedundantCondition it's not redundant with older Psalm version + * @psalm-suppress RedundantCast + */ + $op = (string) self::VERSION_OPERATORS[$operator]; + + if (!$this->packageSatisfiesVersionConstraint('vimeo/psalm', $op . $version)) { + /** @psalm-suppress InternalClass,InternalMethod */ + throw new SkippedTestError("This scenario requires Psalm $op $version because of $reason"); + } + } + + /** + * @Then I see these errors + */ + public function seeTheseErrors(TableNode $list): void + { + /** @psalm-suppress MixedAssignment */ + foreach (array_values($list->getRows()) as $i => $error) { + assert(is_array($error)); + if (0 === $i) { + continue; + } + $this->seeThisError((string) $error[0], (string) $error[1]); + } + } + + /** + * @Given I have the following code in :arg1 :arg2 + */ + public function haveTheFollowingCodeIn(string $filename, string $code): void + { + $file = rtrim($this->config['default_dir'], '/') . '/' . $filename; + $this->fs()->writeToFile($file, $code); + } + + /** + * @Given I have the following autoload map + * @Given I have the following classmap + * @Given I have the following class map + */ + public function haveTheFollowingAutoloadMap(TableNode $list): void + { + $map = []; + /** @psalm-suppress MixedAssignment */ + foreach (array_values($list->getRows()) as $i => $row) { + assert(is_array($row)); + if (0 === $i) { + continue; + } + assert(is_string($row[0])); + assert(is_string($row[1])); + $map[] = [$row[0], $row[1]]; + } + + $code = sprintf( + ' $classes */ + static $classes = null; + if (null === $classes) { + $classes = [%s]; + } + if (array_key_exists($class, $classes)) { + /** @psalm-suppress UnresolvableInclude */ + include $classes[$class]; + } + });', + join( + ',', + array_map( + function (array $row): string { + return "\n'$row[0]' => '$row[1]'"; + }, + $map + ) + ) + ); + $file = rtrim($this->config['default_dir'], '/') . '/' . 'autoload.php'; + $this->fs()->writeToFile($file, $code); + $this->hasAutoload = true; + } + + /** + * @Given I have the :package package satisfying the :versionConstraint + */ + public function haveADependencySatisfied(string $package, string $versionConstraint): void + { + if ($this->packageSatisfiesVersionConstraint($package, $versionConstraint)) { + return; + } + + /** @psalm-suppress InternalClass,InternalMethod */ + throw new SkippedTestError("This scenario requires $package to match $versionConstraint"); + } + + private function convertToRegexp(string $in): string + { + return '@' . str_replace('%', '.*', preg_quote($in, '@')) . '@'; + } + + private function cli(): Cli + { + if (null === $this->cli) { + $cli = $this->getModule('Cli'); + if (!$cli instanceof Cli) { + throw new ModuleRequireException($this, 'Needs Cli module'); + } + $this->cli = $cli; + } + return $this->cli; + } + + private function fs(): Filesystem + { + if (null === $this->fs) { + $fs = $this->getModule('Filesystem'); + if (!$fs instanceof Filesystem) { + throw new ModuleRequireException($this, 'Needs Filesystem module'); + } + $this->fs = $fs; + } + return $this->fs; + } + + private function remainingErrors(): string + { + $this->parseErrors(); + return (string) new TableNode(array_map( + function (array $error): array { + return [ + 'type' => $error['type'] ?? '', + 'message' => $error['message'] ?? '', + ]; + }, + $this->errors + )); + } + + private function getShortVersion(string $package): string + { + /** @psalm-suppress DeprecatedClass Versions is marked deprecated for no good reason */ + if (class_exists(InstalledVersions::class)) { + /** @psalm-suppress UndefinedClass Composer\InstalledVersions is undefined when using Composer 1.x */ + return (string) InstalledVersions::getPrettyVersion($package); + } elseif (class_exists(Versions::class)) { + /** + * @psalm-suppress ArgumentTypeCoercion Versions::getVersion() has too narrow a signature + * @psalm-suppress RedundantCondition not redundant with older Psalm + * @psalm-suppress RedundantCast + */ + $version = (string) Versions::getVersion($package); + } else { + throw new RuntimeException( + 'Cannot determine versions. Neither of composer:2+,' + . ' ocramius/package-version or composer/package-versions-deprecated are installed.' + ); + } + + if (false === strpos($version, '@')) { + throw new RuntimeException('$version must contain @'); + } + + return explode('@', $version)[0]; + } + + /** + * @psalm-assert !null $this->errors + */ + private function parseErrors(): void + { + if (null !== $this->errors) { + return; + } + + if (empty($this->output)) { + $this->errors = []; + return; + } + + /** @psalm-suppress MixedAssignment */ + $errors = json_decode($this->output, true); + + if (null === $errors && json_last_error() !== JSON_ERROR_NONE && 0 !== $this->exitCode) { + Assert::fail("Failed to parse output: " . $this->output . "\nError:" . json_last_error_msg()); + } + + $this->errors = array_map( + function (array $row): array { + return [ + 'type' => (string) $row['type'], + 'message' => (string) $row['message'], + ]; + }, + array_values((array)$errors) + ); + $this->debug($this->remainingErrors()); + } +} diff --git a/tests/acceptance.suite.yml b/tests/acceptance.suite.yml index 046e9300..22405c44 100644 --- a/tests/acceptance.suite.yml +++ b/tests/acceptance.suite.yml @@ -3,4 +3,4 @@ modules: enabled: - Cli - Filesystem - - \Weirdan\Codeception\Psalm\Module + - \Tests\Psalm\LaravelPlugin\Support\Module diff --git a/tests/acceptance/EloquentBuilderTypes.feature b/tests/acceptance/EloquentBuilderTypes.feature index fc3220f2..c84ae0df 100644 --- a/tests/acceptance/EloquentBuilderTypes.feature +++ b/tests/acceptance/EloquentBuilderTypes.feature @@ -132,7 +132,7 @@ Feature: Eloquent Builder types When I run Psalm Then I see no errors - Scenario: + Scenario: Unknown Scenario Given I have the following code preamble """ = 8.0" + Scenario: can call firstOrNew and firstOrCreate without parameters in Laravel 9.x + Given I have the "laravel/framework" package satisfying the "^9.0" And I have the following code """ /** * @psalm-param Builder $builder - * @psalm-return User + * @psalm-return Builder|User */ - function test_firstOrCreate(Builder $builder): User { + function test_firstOrCreate(Builder $builder): Builder|User { return $builder->firstOrCreate(); } /** * @psalm-param Builder $builder - * @psalm-return User + * @psalm-return Builder|User */ - function test_firstOrNew(Builder $builder): User { + function test_firstOrNew(Builder $builder): Builder|User { return $builder->firstOrNew(); } """ diff --git a/tests/acceptance/EloquentCollectionTypes.feature b/tests/acceptance/EloquentCollectionTypes.feature index 94367e5a..214861ad 100644 --- a/tests/acceptance/EloquentCollectionTypes.feature +++ b/tests/acceptance/EloquentCollectionTypes.feature @@ -16,7 +16,7 @@ Feature: Eloquent Collection types """ - Scenario: + Scenario: Unknown Scenario Given I have the following code """ phone()->firstOrCreate(); - } - - function test_hasOne_firstOrNew(User $user): Phone { - return $user->phone()->firstOrNew(); - } - - function test_hasMany_firstOrCreate(Post $post): Comment { - return $post->comments()->firstOrCreate(); - } - - function test_hasMany_firstOrNew(Post $post): Comment { - return $post->comments()->firstOrNew(); - } - """ - When I run Psalm - Then I see these errors - | Type | Message | - | TooFewArguments | Too few arguments for method Illuminate\Database\Eloquent\Relations\HasOneOrMany::firstorcreate saw 0 | - | TooFewArguments | Too few arguments for method Illuminate\Database\Eloquent\Relations\HasOneOrMany::firstornew saw 0 | - | TooFewArguments | Too few arguments for method Illuminate\Database\Eloquent\Relations\HasOneOrMany::firstorcreate saw 0 | - | TooFewArguments | Too few arguments for method Illuminate\Database\Eloquent\Relations\HasOneOrMany::firstornew saw 0 | - - Scenario: can call firstOrNew and firstOrCreate without parameters in Laravel 8.x Given I have the "laravel/framework" package satisfying the ">= 8.0" And I have the following code diff --git a/tests/acceptance/ExceptionHandler.feature b/tests/acceptance/ExceptionHandler.feature index 9d554730..20a950b3 100644 --- a/tests/acceptance/ExceptionHandler.feature +++ b/tests/acceptance/ExceptionHandler.feature @@ -17,7 +17,7 @@ Feature: ExceptionHandler """ - Scenario: + Scenario: Unknown Scenario Given I have the following code """ - */ - public function getFactory(): FactoryBuilder - { - return factory(User::class); - } - - /** - * @return FactoryBuilder - */ - public function getFactoryForTwo(): FactoryBuilder - { - return factory(User::class, 2); - } - - public function makeUser(): User - { - return factory(User::class)->make(); - } - - public function createUser(): User - { - return factory(User::class)->create(); - } - - /** - * @return Collection - */ - public function createUsers(): Collection - { - return factory(User::class, 2)->create(); - } - - /** - * @return Collection - */ - public function createUsersWithNameAttribute(): Collection - { - return factory(User::class, 'new name', 2)->create(); - } - } - """ - When I run Psalm - Then I see no errors - Scenario: cannot use factory helper in Laravel 8.x and later Given I have the "laravel/framework" package satisfying the ">= 8.0" And I have the following code diff --git a/tests/acceptance/RedirectReturnType.feature b/tests/acceptance/RedirectReturnType.feature index 9b66a55b..d48518cf 100644 --- a/tests/acceptance/RedirectReturnType.feature +++ b/tests/acceptance/RedirectReturnType.feature @@ -16,7 +16,7 @@ Feature: redirect() """ - Scenario: + Scenario: Unknown Scenario Given I have the following code """ """ - Scenario: + Scenario: Unknown Scenario Given I have the following code """