diff --git a/README.md b/README.md index b395c98d5..cf421ed08 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,25 @@ -# `phpstan-dba`: PHPStan based SQL static analysis and type inference for the database access layer +phpstan bug repro -`phpstan-dba` makes your phpstan static code analysis jobs aware of datatypes within your database. -With this information at hand we are able to detect type inconsistencies between your domain model and database-schema. -Additionally errors in code handling the results of sql queries can be detected. +- clone the repository +- checkout this PR +- composer install -This extension provides the following features, as long as you [stick to the rules](https://staabm.github.io/2022/07/23/phpstan-dba-inference-placeholder.html#the-golden-phpstan-dba-rules): +`vendor/bin/phpstan analyze src/SqlAst/ParserInference.php --debug` -* [result set type-inference](https://staabm.github.io/2022/06/19/phpstan-dba-type-inference.html) -* [detect errors in sql queries](https://staabm.github.io/2022/08/05/phpstan-dba-syntax-error-detection.html) -* [detect placeholder/bound value mismatches](https://staabm.github.io/2022/07/30/phpstan-dba-placeholder-validation.html) -* [query plan analysis](https://staabm.github.io/2022/08/16/phpstan-dba-query-plan-analysis.html) to detect performance issues -* builtin support for `doctrine/dbal`, `mysqli`, and `PDO` -* API to configure the same features for your custom sql based database access layer -* Opt-In analysis of write queries (since version 0.2.55+) +leads to -In case you are using Doctrine ORM, you might use `phpstan-dba` in tandem with [phpstan-doctrine](https://github.com/phpstan/phpstan-doctrine). - -> [!NOTE] -> At the moment only MySQL/MariaDB and PGSQL databases are supported. Technically it's not a big problem to support other databases though. - -## Talks - -[phpstan-dba - check your sql queries like a boss](https://staabm.github.io/talks/phpstan-dba@phpugffm/) -May 2023, at PHP Usergroup in Frankfurt Main (Germany). - -## DEMO - -see the ['Files Changed' tab of the DEMO-PR](https://github.com/staabm/phpstan-dba/pull/61/files#diff-98a3c43049f6a0c859c0303037d9773534396533d7890bad187d465d390d634e) for a quick glance. - -## 💌 Support phpstan-dba - -[Consider supporting the project](https://github.com/sponsors/staabm), so we can make this tool even better even faster for everyone. - -## Installation - -**First**, use composer to install: - -```shell -composer require --dev staabm/phpstan-dba -``` - -**Second**, create a `phpstan-dba-bootstrap.php` file, which allows to you to configure `phpstan-dba` (this optionally includes database connection details, to introspect the database; if you would rather not do this see [Record and Replay](https://github.com/staabm/phpstan-dba/blob/main/docs/record-and-replay.md): - -```php -debugMode(true); -// $config->stringifyTypes(true); -// $config->analyzeQueryPlans(true); -// $config->utilizeSqlAst(true); - -// TODO: Put your database credentials here -$mysqli = new mysqli('hostname', 'username', 'password', 'database'); - -QueryReflection::setupReflector( - new ReplayAndRecordingQueryReflector( - ReflectionCache::create( - $cacheFile - ), - // XXX alternatively you can use PdoMysqlQueryReflector instead - new MysqliQueryReflector($mysqli), - new SchemaHasherMysql($mysqli) - - ), - $config -); ``` - -> [!NOTE] -> [Configuration for PGSQL](https://github.com/staabm/phpstan-dba/blob/main/docs/pgsql.md) is pretty similar - -**Third**, create or update your `phpstan.neon` file so [bootstrapFiles](https://phpstan.org/config-reference#bootstrap) includes `phpstan-dba-bootstrap.php`. - -If you are **not** using [phpstan/extension-installer](https://github.com/phpstan/extension-installer), you will also need to include `dba.neon`. - -Your `phpstan.neon` might look something like: - -```neon -parameters: - level: 8 - paths: - - src/ - bootstrapFiles: - - phpstan-dba-bootstrap.php - -includes: - - ./vendor/staabm/phpstan-dba/config/dba.neon +➜ phpstan-dba git:(staabm-patch-3) ✗ vendor/bin/phpstan analyze src/SqlAst/ParserInference.php --debug +Note: Using configuration file /Users/staabm/workspace/phpstan-dba/phpstan.neon.dist. +/Users/staabm/workspace/phpstan-dba/src/SqlAst/ParserInference.php + ------ --------------------------------------------- + Line ParserInference.php + ------ --------------------------------------------- + 51 Cannot call method getCondition() on mixed. + 🪪 method.nonObject + 55 Cannot call method getLeft() on mixed. + 🪪 method.nonObject + ------ --------------------------------------------- ``` -**Finally**, run `phpstan`, e.g. - -```shell -./vendor/bin/phpstan analyse -c phpstan.neon -``` - -## Read more - -- [Runtime configuration](https://github.com/staabm/phpstan-dba/blob/main/docs/configuration.md) -- [Record and Replay](https://github.com/staabm/phpstan-dba/blob/main/docs/record-and-replay.md) -- [Custom Query API Support](https://github.com/staabm/phpstan-dba/blob/main/docs/rules.md) -- [MySQL Support](https://github.com/staabm/phpstan-dba/blob/main/docs/mysql.md) -- [PGSQL Support](https://github.com/staabm/phpstan-dba/blob/main/docs/pgsql.md) -- [Reflector Overview](https://github.com/staabm/phpstan-dba/blob/main/docs/reflectors.md) -- [How to analyze a PHP 7.x codebase?](https://github.com/staabm/phpstan-dba/blob/main/docs/faq.md) +-> why does `$from` in `src/SqlAst/ParserInference.php` turn into `mixed`? diff --git a/composer.json b/composer.json index 11323d11d..e32408c51 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "composer-runtime-api": "^2.0", "composer/semver": "^3.2", "doctrine/dbal": "3.*", - "phpstan/phpstan": "^1.9.4" + "phpstan/phpstan": "^2.0", + "sqlftw/sqlftw": "^0.1.16" }, "require-dev": { "ext-mysqli": "*", @@ -16,12 +17,11 @@ "dibi/dibi": "^4.2", "php-parallel-lint/php-parallel-lint": "^1.4", "phpstan/extension-installer": "^1.4", - "phpstan/phpstan-deprecation-rules": "^1.1", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^8.5|^9.5", "symplify/easy-coding-standard": "^12.3", - "tomasvotruba/unused-public": "^1.0", "vlucas/phpdotenv": "^5.4" }, "conflict": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 8ee3611f6..4fc7c9cc2 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,36 +1,8 @@ includes: - - config/extensions.neon - - config/rules.neon - phpstan-baseline.neon parameters: - editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%' - level: max paths: - src/ - - bootstrapFiles: - - bootstrap.php - - unused_public: - methods: true - properties: true - constants: true - - reportUnmatchedIgnoredErrors: false - - ignoreErrors: - - - message: '#^Method staabm\\PHPStanDba\\DbSchema\\SchemaHasherMysql\:\:hashDb\(\) should return string but returns float\|int\|string\.$#' - path: src/DbSchema/SchemaHasherMysql.php - - - message: '#^Property staabm\\PHPStanDba\\DbSchema\\SchemaHasherMysql\:\:\$hash \(string\|null\) does not accept float\|int\|string\.$#' - path: src/DbSchema/SchemaHasherMysql.php - - - message: '#^Instanceof between mysqli_result\\> and mysqli_result will always evaluate to true\.$#' - path: src/DbSchema/SchemaHasherMysql.php - - - message: '#^Instanceof between mysqli_result\|string\|null>> and mysqli_result will always evaluate to true\.$#' - path: src/DbSchema/SchemaHasherMysql.php diff --git a/src/DoctrineReflection/DoctrineResultObjectType.php b/src/DoctrineReflection/DoctrineResultObjectType.php index 4f833d1e3..2e885d80d 100644 --- a/src/DoctrineReflection/DoctrineResultObjectType.php +++ b/src/DoctrineReflection/DoctrineResultObjectType.php @@ -7,6 +7,7 @@ use Doctrine\DBAL\Result; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -51,10 +52,10 @@ public function equals(Type $type): bool return parent::equals($type); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createFromBoolean( + return IsSuperTypeOfResult::createFromBoolean( $type->rowType !== null && $this->rowType !== null && $type->rowType->equals($this->rowType) diff --git a/src/DoctrineReflection/DoctrineStatementObjectType.php b/src/DoctrineReflection/DoctrineStatementObjectType.php index 58be56961..3854700ec 100644 --- a/src/DoctrineReflection/DoctrineStatementObjectType.php +++ b/src/DoctrineReflection/DoctrineStatementObjectType.php @@ -7,6 +7,7 @@ use Doctrine\DBAL\Statement; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -51,10 +52,10 @@ public function equals(Type $type): bool return parent::equals($type); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createFromBoolean( + return IsSuperTypeOfResult::createFromBoolean( $type->rowType !== null && $this->rowType !== null && $type->rowType->equals($this->rowType) diff --git a/src/Extensions/DibiConnectionFetchDynamicReturnTypeExtension.php b/src/Extensions/DibiConnectionFetchDynamicReturnTypeExtension.php index 42b54a0dc..97f873a4d 100644 --- a/src/Extensions/DibiConnectionFetchDynamicReturnTypeExtension.php +++ b/src/Extensions/DibiConnectionFetchDynamicReturnTypeExtension.php @@ -114,7 +114,7 @@ private function reduceResultType(MethodReflection $methodReflection, Type $resu if ('fetch' === $methodName) { return TypeCombinator::addNull($resultType); } elseif ('fetchAll' === $methodName) { - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $resultType)); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $resultType), new AccessoryArrayListType()); } elseif ('fetchPairs' === $methodName && $resultType instanceof ConstantArrayType && 2 === \count($resultType->getValueTypes())) { return new ArrayType($resultType->getValueTypes()[0], $resultType->getValueTypes()[1]); } elseif ('fetchSingle' === $methodName && $resultType instanceof ConstantArrayType && 1 === \count($resultType->getValueTypes())) { diff --git a/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php b/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php index 1418390b3..7d4e8a76f 100644 --- a/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php +++ b/src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php @@ -49,7 +49,7 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod } if (null !== $inferredType) { - return $this->typeSpecifier->create($methodCall->var, $inferredType, TypeSpecifierContext::createTruthy(), true); + return $this->typeSpecifier->create($methodCall->var, $inferredType, TypeSpecifierContext::createTruthy(), $scope)->setAlwaysOverwriteTypes(); } return new SpecifiedTypes(); diff --git a/src/Extensions/PdoStatementSetFetchModeTypeSpecifyingExtension.php b/src/Extensions/PdoStatementSetFetchModeTypeSpecifyingExtension.php index 724f5a7c9..1b5de86c3 100644 --- a/src/Extensions/PdoStatementSetFetchModeTypeSpecifyingExtension.php +++ b/src/Extensions/PdoStatementSetFetchModeTypeSpecifyingExtension.php @@ -45,7 +45,7 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod $reducedType = $this->reduceType($methodCall, $statementType, $scope); if (null !== $reducedType) { - return $this->typeSpecifier->create($methodCall->var, $reducedType, TypeSpecifierContext::createTruthy(), true); + return $this->typeSpecifier->create($methodCall->var, $reducedType, TypeSpecifierContext::createTruthy(), $scope)->setAlwaysOverwriteTypes(); } } diff --git a/src/MysqliReflection/MysqliResultObjectType.php b/src/MysqliReflection/MysqliResultObjectType.php index f0016e480..36aa63f81 100644 --- a/src/MysqliReflection/MysqliResultObjectType.php +++ b/src/MysqliReflection/MysqliResultObjectType.php @@ -5,6 +5,7 @@ namespace staabm\PHPStanDba\MysqliReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -40,10 +41,10 @@ public function equals(Type $type): bool return parent::equals($type); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createFromBoolean( + return IsSuperTypeOfResult::createFromBoolean( $type->rowType !== null && $this->rowType !== null && $type->rowType->equals($this->rowType) diff --git a/src/PdoReflection/PdoStatementObjectType.php b/src/PdoReflection/PdoStatementObjectType.php index 6753ef5b7..ea6a67960 100644 --- a/src/PdoReflection/PdoStatementObjectType.php +++ b/src/PdoReflection/PdoStatementObjectType.php @@ -14,6 +14,7 @@ use PHPStan\Type\FloatType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; @@ -165,10 +166,10 @@ public function equals(Type $type): bool return parent::equals($type); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createFromBoolean( + return IsSuperTypeOfResult::createFromBoolean( $type->fetchType !== null && $type->bothType !== null && $this->bothType !== null diff --git a/src/QueryReflection/BasePdoQueryReflector.php b/src/QueryReflection/BasePdoQueryReflector.php index 2a21375df..7018cabce 100644 --- a/src/QueryReflection/BasePdoQueryReflector.php +++ b/src/QueryReflection/BasePdoQueryReflector.php @@ -53,7 +53,7 @@ abstract class BasePdoQueryReflector implements QueryReflector, RecordingReflect protected const MAX_CACHE_SIZE = 50; /** - * @var array|null> + * @var array|null> */ protected array $cache = []; @@ -69,7 +69,7 @@ abstract class BasePdoQueryReflector implements QueryReflector, RecordingReflect protected $stmt = null; /** - * @var array>> + * @var array>> */ protected array $emulatedFlags = []; @@ -138,7 +138,7 @@ public function getResultType(string $queryString, int $fetchType): ?Type } /** - * @return list + * @return array */ protected function emulateFlags(string $nativeType, string $tableName, string $columnName): array { @@ -175,7 +175,7 @@ public function getDatasource() } /** - * @return PDOException|list|null + * @return PDOException|array|null */ abstract protected function simulateQuery(string $queryString); diff --git a/src/QueryReflection/MysqliQueryReflector.php b/src/QueryReflection/MysqliQueryReflector.php index 7a115355d..9b6445abf 100644 --- a/src/QueryReflection/MysqliQueryReflector.php +++ b/src/QueryReflection/MysqliQueryReflector.php @@ -96,9 +96,13 @@ public function getResultType(string $queryString, int $fetchType): ?Type foreach ($result as $val) { if ( ! property_exists($val, 'name') + || ! is_string($val->name) || ! property_exists($val, 'type') + || ! is_int($val->type) || ! property_exists($val, 'flags') + || ! is_int($val->flags) || ! property_exists($val, 'length') + || ! is_int($val->length) ) { throw new ShouldNotHappenException(); } @@ -127,7 +131,7 @@ public function setupDbaApi(?DbaApi $dbaApi): void } /** - * @return mysqli_sql_exception|list|null + * @return mysqli_sql_exception|array|null */ private function simulateQuery(string $queryString) { diff --git a/src/QueryReflection/PdoMysqlQueryReflector.php b/src/QueryReflection/PdoMysqlQueryReflector.php index d78666b8c..b74f0b3ba 100644 --- a/src/QueryReflection/PdoMysqlQueryReflector.php +++ b/src/QueryReflection/PdoMysqlQueryReflector.php @@ -31,7 +31,7 @@ public function __construct(PDO $pdo) } /** - * @return PDOException|list|null + * @return PDOException|array|null */ protected function simulateQuery(string $queryString) { diff --git a/src/QueryReflection/PdoPgSqlQueryReflector.php b/src/QueryReflection/PdoPgSqlQueryReflector.php index c2765a8ab..7007c3b95 100644 --- a/src/QueryReflection/PdoPgSqlQueryReflector.php +++ b/src/QueryReflection/PdoPgSqlQueryReflector.php @@ -13,7 +13,7 @@ use function array_shift; /** - * @phpstan-type PDOColumnMeta array{name: string, table?: string, native_type: string, len: int, flags: list} + * @phpstan-type PDOColumnMeta array{name: string, table?: string, native_type: string, len: int, flags: array} */ final class PdoPgSqlQueryReflector extends BasePdoQueryReflector { @@ -28,9 +28,9 @@ public function __construct(PDO $pdo) } /** - * @return PDOException|list|null + * @return PDOException|array|null */ - protected function simulateQuery(string $queryString) // @phpstan-ignore-line + protected function simulateQuery(string $queryString) { if (\array_key_exists($queryString, $this->cache)) { return $this->cache[$queryString]; diff --git a/src/QueryReflection/QueryReflection.php b/src/QueryReflection/QueryReflection.php index 332f0f695..8b5326ac9 100644 --- a/src/QueryReflection/QueryReflection.php +++ b/src/QueryReflection/QueryReflection.php @@ -7,8 +7,10 @@ use Composer\InstalledVersions; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Concat; +use PhpParser\Node\InterpolatedStringPart; use PhpParser\Node\Scalar\Encapsed; use PhpParser\Node\Scalar\EncapsedStringPart; +use PhpParser\Node\Scalar\InterpolatedString; use PHPStan\Analyser\Scope; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -206,7 +208,7 @@ public function isResolvable(Expr $queryExpr, Scope $scope): TrinaryLogic } $isStringOrMixed = $type->isSuperTypeOf(new StringType()); - return $isStringOrMixed->negate(); + return $isStringOrMixed->negate()->result; } /** @@ -369,9 +371,14 @@ private function resolveQueryStringExpr(Expr $queryExpr, Scope $scope, bool $res return $leftString . $rightString; } - if ($queryExpr instanceof Encapsed) { + if ($queryExpr instanceof InterpolatedString) { $string = ''; foreach ($queryExpr->parts as $part) { + if ($part instanceof InterpolatedStringPart) { + $string .= $part->value; + continue; + } + $resolvedPart = $this->resolveQueryStringExpr($part, $scope); if (null === $resolvedPart) { return null; @@ -382,10 +389,6 @@ private function resolveQueryStringExpr(Expr $queryExpr, Scope $scope, bool $res return $string; } - if ($queryExpr instanceof EncapsedStringPart) { - return $queryExpr->value; - } - $type = $scope->getType($queryExpr); return QuerySimulation::simulateParamValueType($type, false); diff --git a/src/SqlAst/ParserInference.php b/src/SqlAst/ParserInference.php index 273316574..38371ba77 100644 --- a/src/SqlAst/ParserInference.php +++ b/src/SqlAst/ParserInference.php @@ -1,14 +1,9 @@ -schemaReflection = $schemaReflection; - } - public function narrowResultType(string $queryString, ConstantArrayType $resultType): Type { $platform = Platform::get(Platform::MYSQL, '8.0'); // version defaults to x.x.99 when no patch number is given @@ -53,9 +33,6 @@ public function narrowResultType(string $queryString, ConstantArrayType $resultT $selectColumns = null; $fromTable = null; - $where = null; - $groupBy = null; - $joins = []; foreach ($commands as $command) { // Parser does not throw exceptions. this allows to parse partially invalid code and not fail on first error if ($command instanceof SelectCommand) { @@ -63,66 +40,18 @@ public function narrowResultType(string $queryString, ConstantArrayType $resultT $selectColumns = $command->getColumns(); } $from = $command->getFrom(); - $where = $command->getWhere(); - $groupBy = $command->getGroupBy(); if (null === $from) { // no FROM clause, use an empty Table to signify this $fromTable = new Table('', []); } elseif ($from instanceof TableReferenceTable) { $fromName = $from->getTable()->getName(); - $fromTable = $this->schemaReflection->getTable($fromName); } elseif ($from instanceof Join) { while (1) { if ($from->getCondition() === null) { - if (QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) { - throw new UnresolvableAstInQueryException('Cannot narrow down types null join conditions: ' . $queryString); - } - return $resultType; } - if ($from->getRight() instanceof TableReferenceSubquery || $from->getLeft() instanceof TableReferenceSubquery) { - if (QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) { - throw new UnresolvableAstInQueryException('Cannot narrow down types for SQLs with subqueries: ' . $queryString); - } - - return $resultType; - } - - if ($from instanceof InnerJoin && $from->isCrossJoin()) { - if (QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) { - throw new UnresolvableAstInQueryException('Cannot narrow down types for cross joins: ' . $queryString); - } - - return $resultType; - } - - $joinType = SchemaJoin::TYPE_OUTER; - - if ($from instanceof InnerJoin) { - $joinType = SchemaJoin::TYPE_INNER; - } - - $joinedTable = $this->schemaReflection->getTable($from->getRight()->getTable()->getName()); - - if ($joinedTable !== null) { - $joins[] = new SchemaJoin( - $joinType, - $joinedTable, - $from->getCondition() - ); - } - - if ($from->getLeft() instanceof TableReferenceTable) { - $from = $from->getLeft(); - - $fromName = $from->getTable()->getName(); - $fromTable = $this->schemaReflection->getTable($fromName); - - break; - } - $from = $from->getLeft(); } } @@ -133,73 +62,7 @@ public function narrowResultType(string $queryString, ConstantArrayType $resultT // not parsable atm, return un-narrowed type return $resultType; } - if (null === $selectColumns) { - throw new ShouldNotHappenException(); - } - - $queryScope = new QueryScope($fromTable, $joins, $where, $groupBy !== null); - - // If we're selecting '*', get the selected columns from the table - if (\count($selectColumns) === 1 && $selectColumns[0]->getExpression() instanceof Asterisk) { - $selectColumns = []; - foreach ($fromTable->getColumns() as $column) { - $selectColumns[] = new SelectExpression(new SimpleName($column->getName())); - } - } - - foreach ($selectColumns as $i => $column) { - $expression = $column->getExpression(); - - $offsetType = new ConstantIntegerType($i); - - $nameType = null; - $exprName = self::getIdentifierName($column); - if ($exprName !== null) { - $nameType = new ConstantStringType($exprName); - } - $rawExpressionType = null; - if ($column->getRawExpression() !== null) { - $rawExpressionType = new ConstantStringType($column->getRawExpression()); - } - $aliasOffsetType = null; - if (null !== $column->getAlias()) { - $aliasOffsetType = new ConstantStringType($column->getAlias()); - } - - $valueType = $resultType->getOffsetValueType($offsetType); - - $type = $queryScope->getType($expression); - if (! $type instanceof MixedType) { - $valueType = $type; - } - - if (null !== $rawExpressionType && $resultType->hasOffsetValueType($rawExpressionType)->yes()) { - $resultType = $resultType->setOffsetValueType( - $rawExpressionType, - $valueType - ); - } - if (null !== $aliasOffsetType && $resultType->hasOffsetValueType($aliasOffsetType)->yes()) { - $resultType = $resultType->setOffsetValueType( - $aliasOffsetType, - $valueType - ); - } - if (null !== $nameType && $resultType->hasOffsetValueType($nameType)->yes()) { - $resultType = $resultType->setOffsetValueType( - $nameType, - $valueType - ); - } - if ($resultType->hasOffsetValueType($offsetType)->yes()) { - $resultType = $resultType->setOffsetValueType( - $offsetType, - $valueType - ); - } - } - - return $resultType; + throw new ShouldNotHappenException(); } /** diff --git a/tests/default/data/dibi.php b/tests/default/data/dibi.php index 8efb3a666..d2186c50a 100644 --- a/tests/default/data/dibi.php +++ b/tests/default/data/dibi.php @@ -19,7 +19,7 @@ public function fetch(\Dibi\Connection $connection) public function fetchAll(\Dibi\Connection $connection) { $row = $connection->fetchAll('SELECT email, adaid FROM ada'); - assertType('array}>', $row); + assertType('list}>', $row); } public function fetchPairs(\Dibi\Connection $connection) diff --git a/tests/default/data/runMysqlQuery.php b/tests/default/data/runMysqlQuery.php index e2e65ec80..894e7d81a 100644 --- a/tests/default/data/runMysqlQuery.php +++ b/tests/default/data/runMysqlQuery.php @@ -25,8 +25,8 @@ class Foo public function run(string $prodHostname, DbCredentials $dbCredentials) { $prodDomains = runMysqlQuery('SELECT cmsdomainid FROM cmsdomain WHERE url ="'.$prodHostname.'" and standard=1', $dbCredentials); - assertType('array>|null', $prodDomains); + assertType('list>|null', $prodDomains); // XXX should be more precise, when we would be smarter in query parsing - // assertType('array|null', $prodDomains); + // assertType('list|null', $prodDomains); } } diff --git a/tests/rules/DoctrineKeyValueStyleRuleTest.php b/tests/rules/DoctrineKeyValueStyleRuleTest.php index 9bbe4d42a..17c8e1753 100644 --- a/tests/rules/DoctrineKeyValueStyleRuleTest.php +++ b/tests/rules/DoctrineKeyValueStyleRuleTest.php @@ -27,6 +27,12 @@ public static function getAdditionalConfigFiles(): array public function testRule(): void { + $err56 = 'Query error: Column "ada.adaid" expects value type int, got type mixed'; + if (PHP_VERSION_ID < 80000) { + // PHP 7.x does not support native mixed type + $err56 = 'Query error: Column "ada.adaid" expects value type int, got type DoctrineKeyValueStyleRuleTest\mixed'; + } + $expectedErrors = [ [ 'Argument #0 expects a constant string, got string', @@ -65,7 +71,7 @@ public function testRule(): void 51, ], [ - 'Query error: Column "ada.adaid" expects value type int, got type mixed', + $err56, 56, ], [