diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index f34d05ef0..506207a44 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -42,12 +42,6 @@ code = "mixed-assignment" message = "Assigning `mixed` type to a variable may lead to unexpected behavior." count = 1 -[[issues]] -file = "src/Adapter/Adapter.php" -code = "possibly-null-argument" -message = 'Argument #1 of method `PhpDb\Adapter\Driver\DriverInterface::createStatement` is possibly `null`, but parameter type `resource|string` does not accept it.' -count = 1 - [[issues]] file = "src/Adapter/AdapterAwareInterface.php" code = "missing-api-or-internal" @@ -336,12 +330,6 @@ code = "missing-api-or-internal" message = 'Abstract class `PhpDb\Adapter\Driver\Pdo\AbstractPdo` is missing an `@api` or `@internal` annotation.' count = 1 -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "mixed-return-statement" -message = 'Could not infer a precise return type for function `PhpDb\Adapter\Driver\Pdo\AbstractPdo::getLastGeneratedValue`. Saw type `mixed`.' -count = 1 - [[issues]] file = "src/Adapter/Driver/Pdo/AbstractPdo.php" code = "non-existent-method" @@ -420,12 +408,6 @@ code = "possible-method-access-on-null" message = "Attempting to call a method on `null`." count = 7 -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" -code = "possibly-null-argument" -message = 'Argument #1 of method `PhpDb\Adapter\Driver\DriverInterface::createStatement` is possibly `null`, but parameter type `resource|string` does not accept it.' -count = 1 - [[issues]] file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" code = "too-many-arguments" @@ -474,18 +456,6 @@ code = "incompatible-return-type" message = 'Return type `mixed` of `PhpDb\Adapter\Driver\Pdo\Result::next()` is incompatible with parent return type `void` of `Iterator::next()`' count = 1 -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "less-specific-nested-argument-type" -message = 'Argument type mismatch for argument #1 of `PhpDb\ResultSet\AbstractResultSet::initialize`: expected `iterable`, but provided type `$this(PhpDb\Adapter\Driver\Pdo\Result)` is less specific.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "less-specific-nested-argument-type" -message = 'Argument type mismatch for argument #1 of `PhpDb\ResultSet\ResultSetInterface::initialize`: expected `iterable`, but provided type `$this(PhpDb\Adapter\Driver\Pdo\Result)` is less specific.' -count = 1 - [[issues]] file = "src/Adapter/Driver/Pdo/Result.php" code = "missing-constant-type" @@ -612,12 +582,6 @@ code = "class-must-be-final" message = 'Class `PhpDb\Adapter\Driver\Pdo\Statement` should be declared `final`.' count = 1 -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "docblock-type-mismatch" -message = "Docblock return type `false|null|resource` is incompatible with native return type `PDOStatement|false|null`." -count = 1 - [[issues]] file = "src/Adapter/Driver/Pdo/Statement.php" code = "imprecise-type" @@ -636,42 +600,18 @@ code = "invalid-argument" message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Driver\DriverInterface::createResult`: expected `resource`, but found `PDOStatement|false|null`.' count = 1 -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "invalid-method-access" -message = "Attempting to access a method on a non-object type (`false`)." -count = 3 - [[issues]] file = "src/Adapter/Driver/Pdo/Statement.php" code = "invalid-property-assignment-value" message = "Invalid type for property `$sql`: expected `string`, but got `null|string`." count = 1 -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Adapter\Driver\Pdo\Statement::getResource`: expected `false|null|resource`, but found `PDOStatement|false|null`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "mixed-argument" -message = "Invalid argument type for argument #2 of `implode`: expected `array|null`, but found `mixed`." -count = 1 - [[issues]] file = "src/Adapter/Driver/Pdo/Statement.php" code = "mixed-assignment" message = "Assigning `mixed` type to a variable may lead to unexpected behavior." count = 2 -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "possible-method-access-on-null" -message = "Attempting to call a method on `null`." -count = 3 - [[issues]] file = "src/Adapter/Driver/Pdo/Statement.php" code = "possibly-false-argument" @@ -1410,12 +1350,6 @@ code = "mixed-assignment" message = "Assigning `mixed` type to a variable may lead to unexpected behavior." count = 2 -[[issues]] -file = "src/ResultSet/AbstractResultSet.php" -code = "no-value" -message = 'Argument #1 passed to method `PhpDb\ResultSet\AbstractResultSet::initializeFromResult` has type `never`, meaning it cannot produce a value.' -count = 1 - [[issues]] file = "src/ResultSet/Exception/InvalidArgumentException.php" code = "class-must-be-final" @@ -1428,6 +1362,24 @@ code = "class-must-be-final" message = 'Class `PhpDb\ResultSet\Exception\RuntimeException` should be declared `final`.' count = 1 +[[issues]] +file = "src/ResultSet/HydratingResultSet.php" +code = "redundant-docblock-type" +message = "Redundant docblock type for variable `$row`." +count = 1 + +[[issues]] +file = "src/ResultSet/ObjectResultSet.php" +code = "redundant-docblock-type" +message = "Redundant docblock type for variable `$row`." +count = 1 + +[[issues]] +file = "src/ResultSet/RowPrototypeResultSet.php" +code = "redundant-condition" +message = "Redundant ternary operator: condition is always truthy." +count = 1 + [[issues]] file = "src/RowGateway/AbstractRowGateway.php" code = "imprecise-type" @@ -1482,12 +1434,6 @@ code = "incompatible-parameter-type" message = 'Parameter `$offset` of `PhpDb\RowGateway\AbstractRowGateway::offsetunset()` expects type `string` but parent `ArrayAccess::offsetunset()` expects type `mixed`' count = 1 -[[issues]] -file = "src/RowGateway/AbstractRowGateway.php" -code = "invalid-array-element-key" -message = "Invalid array key type." -count = 1 - [[issues]] file = "src/RowGateway/AbstractRowGateway.php" code = "less-specific-nested-argument-type" @@ -1560,6 +1506,24 @@ code = "mixed-argument" message = 'Invalid argument type for argument #1 of `PhpDb\RowGateway\Exception\RuntimeException::forMissingPrimaryKeyData`: expected `string`, but found `mixed`.' count = 1 +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "mixed-array-index" +message = "Invalid index type `mixed` used in array assignment on `array`." +count = 4 + +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "mixed-array-index" +message = "Invalid index type `mixed` used in array assignment on `array`." +count = 1 + +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "mixed-array-index" +message = "Possibly invalid array key type." +count = 1 + [[issues]] file = "src/RowGateway/AbstractRowGateway.php" code = "mixed-assignment" @@ -1728,30 +1692,6 @@ code = "missing-constructor" message = 'Class `PhpDb\Sql\Platform\AbstractPlatform` has typed properties without default values but no constructor to initialize them.' count = 1 -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "mixed-method-access" -message = "Attempting to access a method on a non-object type (`mixed`)." -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "mixed-return-statement" -message = 'Could not infer a precise return type for function `PhpDb\Sql\Platform\AbstractPlatform::getSqlString`. Saw type `mixed`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "mixed-return-statement" -message = 'Could not infer a precise return type for function `PhpDb\Sql\Platform\AbstractPlatform::getTypeDecorator`. Saw type `mixed`.' -count = 1 - [[issues]] file = "src/Sql/Platform/AbstractPlatform.php" code = "non-existent-method" @@ -1794,12 +1734,6 @@ code = "mixed-return-statement" message = 'Could not infer a precise return type for function `PhpDb\Sql\Platform\Platform::getDecorators`. Saw type `mixed`.' count = 1 -[[issues]] -file = "src/Sql/Platform/Platform.php" -code = "mixed-return-statement" -message = 'Could not infer a precise return type for function `PhpDb\Sql\Platform\Platform::getSqlString`. Saw type `mixed`.' -count = 1 - [[issues]] file = "src/Sql/Platform/Platform.php" code = "non-existent-method" diff --git a/lint-baseline.toml b/lint-baseline.toml index 24e5d9407..893a1b21f 100644 --- a/lint-baseline.toml +++ b/lint-baseline.toml @@ -66,12 +66,6 @@ code = "too-many-methods" message = "Class has too many methods." count = 1 -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "halstead" -message = "Method has a high halstead difficulty" -count = 1 - [[issues]] file = "src/Adapter/Driver/Pdo/Statement.php" code = "literal-named-argument" diff --git a/src/Adapter/Driver/Pdo/Statement.php b/src/Adapter/Driver/Pdo/Statement.php index fd52991f2..79ace82c7 100644 --- a/src/Adapter/Driver/Pdo/Statement.php +++ b/src/Adapter/Driver/Pdo/Statement.php @@ -70,10 +70,12 @@ public function execute(array|ParameterContainer|null $parameters = null): ?Resu } /** END Standard ParameterContainer Merging Block */ + $resource = $this->getPreparedResource(); + $this->profiler?->profilerStart($this); try { - $this->resource->execute(); + $resource->execute(); } catch (PDOException $e) { $this->profiler?->profilerFinish(); @@ -83,7 +85,7 @@ public function execute(array|ParameterContainer|null $parameters = null): ?Resu } throw Exception\InvalidQueryException::forFailedStatement( - implode(' - ', $this->resource->errorInfo()), + implode(' - ', $resource->errorInfo()), $code, $e, ); @@ -195,6 +197,7 @@ public function setSql(?string $sql): StatementContainerInterface /** Bind parameters from container */ protected function bindParametersFromContainer(): void { + $resource = $this->getPreparedResource(); $parameters = $this->parameterContainer->getNamedArray(); $errata = $this->parameterContainer->getErrataIterator()->getArrayCopy(); @@ -224,12 +227,20 @@ protected function bindParametersFromContainer(): void $parameter = ':' . ltrim($name, ':'); } - $this->resource->bindParam($parameter, $value, $type); + $resource->bindParam($parameter, $value, $type); } $this->parametersBound = true; } + /** @throws Exception\RuntimeException */ + private function getPreparedResource(): PDOStatement + { + return $this->resource instanceof PDOStatement + ? $this->resource + : throw Exception\RuntimeException::forUnpreparedStatement(); + } + /** Perform a deep clone */ public function __clone(): void { diff --git a/src/Adapter/Exception/RuntimeException.php b/src/Adapter/Exception/RuntimeException.php index f4b1e2120..d0361c55d 100644 --- a/src/Adapter/Exception/RuntimeException.php +++ b/src/Adapter/Exception/RuntimeException.php @@ -45,6 +45,8 @@ class RuntimeException extends Exception\RuntimeException final public const string UNCOMPOSABLE_TRAIT = '%s can only be composed into %s'; + final public const string UNPREPARED_STATEMENT = 'This statement has not been prepared'; + final public const string UNSTARTED_PROFILE = 'A profile must be started before %s can be called.'; final public const string VULNERABLE_PLATFORM_QUOTE = @@ -107,6 +109,11 @@ public static function forUncomposableTrait(string $trait, string $interface): s return new self(sprintf(self::UNCOMPOSABLE_TRAIT, $trait, $interface)); } + public static function forUnpreparedStatement(): self + { + return new self(self::UNPREPARED_STATEMENT); + } + public static function forUnstartedProfile(string $function): self { return new self(sprintf(self::UNSTARTED_PROFILE, $function)); diff --git a/src/Sql/Platform/AbstractPlatform.php b/src/Sql/Platform/AbstractPlatform.php index 20b6ac48c..ac1c69baf 100644 --- a/src/Sql/Platform/AbstractPlatform.php +++ b/src/Sql/Platform/AbstractPlatform.php @@ -44,6 +44,7 @@ public function getSqlString(?PlatformInterface $adapterPlatform = null): string public function getTypeDecorator( PreparableSqlInterface|SqlInterface $subject, ): PlatformDecoratorInterface|PreparableSqlInterface|SqlInterface { + /** @var PlatformDecoratorInterface $decorator */ foreach ($this->decorators as $type => $decorator) { /** @phpstan-ignore-next-line instanceof with string class name is valid */ if (! $subject instanceof $type) { diff --git a/src/Sql/Update.php b/src/Sql/Update.php index ee9e6020b..75225a6f7 100644 --- a/src/Sql/Update.php +++ b/src/Sql/Update.php @@ -217,13 +217,14 @@ protected function processSet( continue; } - $setSql[] = $prefix - . $this->resolveColumnValue( - $value, - $platform, - $driver, - $parameterContainer, - ); + $setSql[] = + $prefix + . $this->resolveColumnValue( + $value, + $platform, + $driver, + $parameterContainer, + ); } return $this->formatSpecification( diff --git a/test/unit/Adapter/Driver/Pdo/StatementTest.php b/test/unit/Adapter/Driver/Pdo/StatementTest.php index 0ad71ca36..572c9065b 100644 --- a/test/unit/Adapter/Driver/Pdo/StatementTest.php +++ b/test/unit/Adapter/Driver/Pdo/StatementTest.php @@ -96,6 +96,16 @@ public function bindParametersDetectsNullValueType(): void static::assertInstanceOf(Result::class, $result); } + #[Test] + public function bindParametersThrowsWhenMarkedPreparedWithoutResource(): void + { + $this->statement->setParameterContainer(new ParameterContainer(['val' => 1])); + (new ReflectionProperty($this->statement, 'isPrepared'))->setValue($this->statement, true); + + $this->expectExceptionObject(RuntimeException::forUnpreparedStatement()); + $this->statement->execute(); + } + #[Test] public function bindParametersWithErrataTypeDefaultsToString(): void { @@ -350,6 +360,15 @@ public function executeThrowsOnInvalidParameterName(string $name): void $this->statement->execute([$name => 'value']); } + #[Test] + public function executeThrowsWhenMarkedPreparedWithoutResource(): void + { + (new ReflectionProperty($this->statement, 'isPrepared'))->setValue($this->statement, true); + + $this->expectExceptionObject(RuntimeException::forUnpreparedStatement()); + $this->statement->execute(); + } + #[Test] public function executeWithArrayParametersMergesIntoContainer(): void { diff --git a/test/unit/Adapter/Exception/RuntimeExceptionTest.php b/test/unit/Adapter/Exception/RuntimeExceptionTest.php index 78ece44b5..df4dc2c34 100644 --- a/test/unit/Adapter/Exception/RuntimeExceptionTest.php +++ b/test/unit/Adapter/Exception/RuntimeExceptionTest.php @@ -29,6 +29,7 @@ #[CoversMethod(RuntimeException::class, 'forNonQueryResult')] #[CoversMethod(RuntimeException::class, 'forRollbackWithoutTransaction')] #[CoversMethod(RuntimeException::class, 'forUncomposableTrait')] +#[CoversMethod(RuntimeException::class, 'forUnpreparedStatement')] #[CoversMethod(RuntimeException::class, 'forUnstartedProfile')] #[CoversMethod(RuntimeException::class, 'forVulnerablePlatformQuote')] final class RuntimeExceptionTest extends TestCase @@ -92,6 +93,11 @@ public static function namedConstructorProvider(): array [DriverFeatureProviderTrait::class, DriverInterface::class], RuntimeException::UNCOMPOSABLE_TRAIT, ], + 'unprepared statement' => [ + 'forUnpreparedStatement', + [], + RuntimeException::UNPREPARED_STATEMENT, + ], 'unstarted profile' => [ 'forUnstartedProfile', ['profilerFinish'],