diff --git a/composer.json b/composer.json index 5803e9ed8..082652401 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "webmozart/path-util": "^2.3", "symfony/console": "^3.0||^4.0", "amphp/amp": "^2.1", - "amphp/byte-stream": "^1.5" + "amphp/byte-stream": "^1.5", + "phpmyadmin/sql-parser": "^4.0" }, "bin": ["psalm", "psalter", "psalm-language-server", "psalm-plugin"], "autoload": { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index ab8aa0020..19bdffe10 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -876,7 +876,8 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ $stmt->name->name, $stmt->args, $context, - new CodeLocation($statements_analyzer->getSource(), $stmt->name) + new CodeLocation($statements_analyzer->getSource(), $stmt->name), + $lhs_type_part instanceof TGenericObject ? $lhs_type_part->type_params : null ); } diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 0ba0076be..3a40af46b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -119,7 +119,7 @@ class ExpressionAnalyzer } elseif ($stmt instanceof PhpParser\Node\Expr\ConstFetch) { ConstFetchAnalyzer::analyze($statements_analyzer, $stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Scalar\String_) { - $stmt->inferredType = Type::getString(strlen($stmt->value) < 50 ? $stmt->value : null); + $stmt->inferredType = Type::getString($stmt->value); } elseif ($stmt instanceof PhpParser\Node\Scalar\EncapsedStringPart) { // do nothing } elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst) { diff --git a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php index 30db8d85d..eec2f2657 100644 --- a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php @@ -20,7 +20,8 @@ class MethodReturnTypeProvider * string, * array, * Context, - * CodeLocation + * CodeLocation, + * ?array= * ) : ?Type\Union> * > */ @@ -68,7 +69,8 @@ class MethodReturnTypeProvider * string, * array, * Context, - * CodeLocation + * CodeLocation, + * ?array= * ) : ?Type\Union $c * * @return void @@ -85,6 +87,7 @@ class MethodReturnTypeProvider /** * @param array $call_args + * @param ?array $template_type_parameters * @return ?Type\Union */ public function getReturnType( @@ -93,7 +96,8 @@ class MethodReturnTypeProvider string $method_name, array $call_args, Context $context, - CodeLocation $code_location + CodeLocation $code_location, + array $template_type_parameters = null ) { foreach (self::$handlers[strtolower($fq_classlike_name)] as $class_handler) { $result = $class_handler( @@ -102,7 +106,8 @@ class MethodReturnTypeProvider strtolower($method_name), $call_args, $context, - $code_location + $code_location, + $template_type_parameters ); if ($result) { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DomNodeAppendChild.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DomNodeAppendChild.php index 0ff807414..618ef445e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/DomNodeAppendChild.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DomNodeAppendChild.php @@ -25,7 +25,8 @@ class DomNodeAppendChild implements \Psalm\Plugin\Hook\MethodReturnTypeProviderI string $method_name_lowercase, array $call_args, Context $context, - CodeLocation $code_location + CodeLocation $code_location, + array $template_type_parameters = null ) { if ($method_name_lowercase === 'appendchild' && isset($call_args[0]->value->inferredType) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/SimpleXmlElementAsXml.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/SimpleXmlElementAsXml.php index 7e5a195c7..3859cdd3a 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/SimpleXmlElementAsXml.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/SimpleXmlElementAsXml.php @@ -25,7 +25,8 @@ class SimpleXmlElementAsXml implements \Psalm\Plugin\Hook\MethodReturnTypeProvid string $method_name_lowercase, array $call_args, Context $context, - CodeLocation $code_location + CodeLocation $code_location, + array $template_type_parameters = null ) { if ($method_name_lowercase === 'asxml' && !count($call_args) diff --git a/src/Psalm/Plugin/Hook/MethodReturnTypeProviderInterface.php b/src/Psalm/Plugin/Hook/MethodReturnTypeProviderInterface.php index 7c67e7124..6e6d6ee72 100644 --- a/src/Psalm/Plugin/Hook/MethodReturnTypeProviderInterface.php +++ b/src/Psalm/Plugin/Hook/MethodReturnTypeProviderInterface.php @@ -17,6 +17,7 @@ interface MethodReturnTypeProviderInterface /** * @param array $call_args + * @param ?array $template_type_parameters * @return ?Type\Union */ public static function getMethodReturnType( @@ -25,6 +26,7 @@ interface MethodReturnTypeProviderInterface string $method_name_lowercase, array $call_args, Context $context, - CodeLocation $code_location + CodeLocation $code_location, + array $template_type_parameters = null ); } diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 95d3f4539..0980d0dda 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -29,6 +29,7 @@ use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TResource; use Psalm\Type\Atomic\TSingleLetter; +use Psalm\Type\Atomic\TSqlSelectString; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTrue; use Psalm\Type\Atomic\TVoid; @@ -923,9 +924,23 @@ abstract class Type */ public static function getString($value = null) { + $type = null; + if ($value !== null) { - $type = new TLiteralString($value); - } else { + if (stripos($value, 'select ') === 0) { + $parser = new \PhpMyAdmin\SqlParser\Parser($value); + + if (!$parser->errors) { + $type = new TSqlSelectString($value); + } + } + + if (!$type && strlen($value) < 50) { + $type = new TLiteralString($value); + } + } + + if (!$type) { $type = new TString(); } diff --git a/src/Psalm/Type/Atomic/TSqlSelectString.php b/src/Psalm/Type/Atomic/TSqlSelectString.php new file mode 100644 index 000000000..65ad7cadd --- /dev/null +++ b/src/Psalm/Type/Atomic/TSqlSelectString.php @@ -0,0 +1,29 @@ +value . ')'; + } + + /** + * @return bool + */ + public function canBeFullyExpressedInPhp() + { + return false; + } +} diff --git a/tests/Plugin/Hook/FooMethodProvider.php b/tests/Plugin/Hook/FooMethodProvider.php index 13de1f36c..ab5e4d3b0 100644 --- a/tests/Plugin/Hook/FooMethodProvider.php +++ b/tests/Plugin/Hook/FooMethodProvider.php @@ -78,7 +78,8 @@ class FooMethodProvider implements string $method_name, array $call_args, Context $context, - CodeLocation $code_location + CodeLocation $code_location, + array $templated_type_parameters = null ) { return Type::getString(); } diff --git a/tests/Traits/ValidCodeAnalysisTestTrait.php b/tests/Traits/ValidCodeAnalysisTestTrait.php index e0bce4113..dd58c4ab8 100644 --- a/tests/Traits/ValidCodeAnalysisTestTrait.php +++ b/tests/Traits/ValidCodeAnalysisTestTrait.php @@ -61,8 +61,19 @@ trait ValidCodeAnalysisTestTrait $actual_vars = []; foreach ($assertions as $var => $_) { + $exact = false; + + if ($var && strpos($var, '===') === strlen($var) - 3) { + $var = substr($var, 0, -3); + $exact = true; + } + if (isset($context->vars_in_scope[$var])) { - $actual_vars[$var] = (string)$context->vars_in_scope[$var]; + if ($exact) { + $actual_vars[$var . '==='] = $context->vars_in_scope[$var]->getId(); + } else { + $actual_vars[$var] = (string)$context->vars_in_scope[$var]; + } } } diff --git a/tests/ValueTest.php b/tests/ValueTest.php index 8a5aa5075..d8fa88865 100644 --- a/tests/ValueTest.php +++ b/tests/ValueTest.php @@ -468,6 +468,17 @@ class ValueTest extends TestCase 'assertions' => [], 'error_levels' => ['MissingParamType', 'MixedAssignment'], ], + 'sqlTypes' => [ + ' [ + '$a===' => 'sql-select-string(select * from foo)', + '$b===' => 'string(select * from)', + '$c===' => 'sql-select-string(select * from foo where i = :i)', + ] + ], ]; }