2016-08-13 23:26:45 -04:00
< ? php
namespace Psalm\Checker ;
ini_set ( 'xdebug.max_nesting_level' , 512 );
use PhpParser ;
2016-10-15 00:12:57 -04:00
use PhpParser\Node\Expr\Closure ;
use PhpParser\Node\Stmt\Function_ ;
use PhpParser\Node\Stmt\ClassMethod ;
2016-08-13 23:26:45 -04:00
use Psalm\Issue\InvalidDocblock ;
use Psalm\Issue\InvalidReturnType ;
2016-10-15 18:01:04 -04:00
use Psalm\Issue\MethodSignatureMismatch ;
2016-10-09 17:54:58 -04:00
use Psalm\FunctionLikeParameter ;
2016-08-13 23:26:45 -04:00
use Psalm\StatementsSource ;
use Psalm\Type ;
use Psalm\Config ;
use Psalm\Context ;
use Psalm\IssueBuffer ;
abstract class FunctionLikeChecker implements StatementsSource
{
2016-10-14 00:53:43 -04:00
/**
2016-10-15 00:12:57 -04:00
* @ var Closure | Function_ | ClassMethod
2016-10-14 00:53:43 -04:00
*/
2016-08-13 23:26:45 -04:00
protected $function ;
2016-10-14 00:53:43 -04:00
/**
* @ var string
*/
2016-08-13 23:26:45 -04:00
protected $namespace ;
2016-10-14 00:53:43 -04:00
/**
* @ var string
*/
2016-08-13 23:26:45 -04:00
protected $file_name ;
2016-10-14 00:53:43 -04:00
/**
2016-10-15 00:12:57 -04:00
* @ var string | null
2016-10-14 00:53:43 -04:00
*/
2016-08-24 19:00:44 -04:00
protected $include_file_name ;
2016-10-14 00:53:43 -04:00
/**
* @ var bool
*/
2016-08-13 23:26:45 -04:00
protected $is_static = false ;
2016-10-14 00:53:43 -04:00
/**
* @ var string
*/
2016-08-13 23:26:45 -04:00
protected $absolute_class ;
2016-10-14 00:53:43 -04:00
/**
* @ var StatementsChecker | null
*/
2016-08-13 23:26:45 -04:00
protected $statements_checker ;
2016-10-14 00:53:43 -04:00
/**
* @ var StatementsSource
*/
2016-08-13 23:26:45 -04:00
protected $source ;
2016-10-14 00:53:43 -04:00
2016-10-15 00:12:57 -04:00
/**
* @ var array < string , array < string , Psalm\Type\Union >>
*/
2016-08-13 23:26:45 -04:00
protected $return_vars_in_scope = [];
2016-10-15 00:12:57 -04:00
/**
* @ var array < string , array < string , bool >>
*/
2016-08-13 23:26:45 -04:00
protected $return_vars_possibly_in_scope = [];
protected $class_name ;
2016-10-09 17:54:58 -04:00
/**
* @ var string | null
*/
2016-08-13 23:26:45 -04:00
protected $class_extends ;
/**
* @ var array
*/
protected $suppressed_issues ;
protected static $no_effects_hashes = [];
2016-10-15 00:12:57 -04:00
/**
* @ param Closure | Function_ | ClassMethod $function
* @ param StatementsSource $source
*/
public function __construct ( $function , StatementsSource $source )
2016-08-13 23:26:45 -04:00
{
$this -> function = $function ;
$this -> namespace = $source -> getNamespace ();
$this -> class_name = $source -> getClassName ();
$this -> class_extends = $source -> getParentClass ();
$this -> file_name = $source -> getFileName ();
2016-08-24 19:00:44 -04:00
$this -> include_file_name = $source -> getIncludeFileName ();
2016-08-13 23:26:45 -04:00
$this -> absolute_class = $source -> getAbsoluteClass ();
$this -> source = $source ;
$this -> suppressed_issues = $source -> getSuppressedIssues ();
}
public function check ( Context $context , $check_methods = true )
{
2016-10-14 00:53:43 -04:00
if ( $function_stmts = $this -> function -> getStmts ()) {
2016-08-13 23:26:45 -04:00
$has_context = ( bool ) count ( $context -> vars_in_scope );
2016-09-01 18:02:09 -04:00
$statements_checker = new StatementsChecker ( $this , $has_context , $check_methods );
2016-10-03 11:38:59 -04:00
$hash = null ;
2016-08-15 15:14:28 -04:00
if ( $this instanceof MethodChecker ) {
2016-08-13 23:26:45 -04:00
if ( ClassLikeChecker :: getThisClass ()) {
$hash = $this -> getMethodId () . json_encode ([ $context -> vars_in_scope , $context -> vars_possibly_in_scope ]);
// if we know that the function has no effects on vars, we don't bother rechecking
if ( isset ( self :: $no_effects_hashes [ $hash ])) {
list ( $context -> vars_in_scope , $context -> vars_possibly_in_scope ) = self :: $no_effects_hashes [ $hash ];
return ;
}
}
elseif ( $context -> self ) {
2016-10-13 18:27:23 -04:00
$context -> vars_in_scope [ '$this' ] = new Type\Union ([ new Type\Atomic ( $context -> self )]);
2016-08-13 23:26:45 -04:00
}
2016-10-15 00:12:57 -04:00
$function_params = MethodChecker :: getMethodParams (( string ) $this -> getMethodId ());
2016-10-15 18:01:04 -04:00
$implemented_method_ids = MethodChecker :: getOverriddenMethodIds (( string ) $this -> getMethodId ());
if ( $implemented_method_ids ) {
2016-10-18 16:00:03 -04:00
$have_emitted = false ;
2016-10-15 18:01:04 -04:00
foreach ( $implemented_method_ids as $implemented_method_id ) {
2016-10-18 16:00:03 -04:00
if ( $have_emitted ) {
break ;
}
2016-10-15 18:45:31 -04:00
if ( $implemented_method_id === 'ArrayObject::__construct' ) {
continue ;
}
2016-10-15 18:01:04 -04:00
$implemented_params = MethodChecker :: getMethodParams ( $implemented_method_id );
foreach ( $implemented_params as $i => $implemented_param ) {
if ( ! isset ( $function_params [ $i ])) {
$cased_method_id = MethodChecker :: getCasedMethodId (( string ) $this -> getMethodId ());
$parent_method_id = MethodChecker :: getCasedMethodId ( $implemented_method_id );
if ( IssueBuffer :: accepts (
new MethodSignatureMismatch (
2016-10-15 18:45:31 -04:00
'Method ' . $cased_method_id . ' has fewer arguments than parent method ' . $parent_method_id ,
2016-10-15 18:01:04 -04:00
$this -> getCheckedFileName (),
$this -> function -> getLine ()
)
)) {
return false ;
}
2016-10-18 16:00:03 -04:00
$have_emitted = true ;
2016-10-15 18:01:04 -04:00
break ;
}
if (( string ) $function_params [ $i ] -> signature_type !== ( string ) $implemented_param -> signature_type ) {
$cased_method_id = MethodChecker :: getCasedMethodId (( string ) $this -> getMethodId ());
$parent_method_id = MethodChecker :: getCasedMethodId ( $implemented_method_id );
if ( IssueBuffer :: accepts (
new MethodSignatureMismatch (
'Argument ' . ( $i + 1 ) . ' of ' . $cased_method_id . ' has wrong type \'' . $function_params [ $i ] -> signature_type . '\', expecting \'' . $implemented_param -> signature_type . '\' as defined by ' . $parent_method_id ,
$this -> getCheckedFileName (),
$this -> function -> getLine ()
)
)) {
return false ;
}
2016-10-18 16:00:03 -04:00
$have_emitted = true ;
2016-10-15 18:01:04 -04:00
break ;
}
}
}
}
2016-09-01 18:02:09 -04:00
}
else if ( $this instanceof FunctionChecker ) {
2016-10-15 00:12:57 -04:00
$function_params = FunctionChecker :: getParams (( string ) $this -> getMethodId (), $this -> file_name );
2016-08-13 23:26:45 -04:00
}
2016-09-01 18:02:09 -04:00
else { // Closure
$function_params = [];
2016-10-15 18:01:04 -04:00
foreach ( $this -> function -> getParams () as $param ) {
$function_params [] = self :: getParamArray ( $param , $this -> absolute_class , $this -> namespace , $this -> getAliasedClasses ());
2016-08-13 23:26:45 -04:00
}
}
2016-09-01 18:02:09 -04:00
foreach ( $function_params as $function_param ) {
$param_type = StatementsChecker :: fleshOutTypes (
2016-10-09 17:54:58 -04:00
clone $function_param -> type ,
2016-09-01 18:02:09 -04:00
[],
$context -> self ,
$this -> getMethodId ()
);
foreach ( $param_type -> types as $atomic_type ) {
if ( $atomic_type -> isObjectType ()
2016-09-01 20:27:16 -04:00
&& ! $atomic_type -> isObject ()
2016-10-15 00:12:57 -04:00
&& $this -> function instanceof PhpParser\Node
2016-09-01 18:02:09 -04:00
&& ClassLikeChecker :: checkAbsoluteClassOrInterface (
$atomic_type -> value ,
$this -> file_name ,
$this -> function -> getLine (),
$this -> suppressed_issues
) === false
) {
return false ;
}
}
2016-10-13 18:27:23 -04:00
$context -> vars_in_scope [ '$' . $function_param -> name ] = $param_type ;
2016-09-01 18:02:09 -04:00
2016-10-15 00:12:57 -04:00
$statements_checker -> registerVariable ( $function_param -> name , $function_param -> line );
2016-09-01 18:02:09 -04:00
}
2016-08-13 23:26:45 -04:00
2016-10-14 00:53:43 -04:00
$statements_checker -> check ( $function_stmts , $context );
2016-08-13 23:26:45 -04:00
if ( isset ( $this -> return_vars_in_scope [ '' ])) {
$context -> vars_in_scope = TypeChecker :: combineKeyedTypes ( $context -> vars_in_scope , $this -> return_vars_in_scope [ '' ]);
}
if ( isset ( $this -> return_vars_possibly_in_scope [ '' ])) {
$context -> vars_possibly_in_scope = array_merge ( $context -> vars_possibly_in_scope , $this -> return_vars_possibly_in_scope [ '' ]);
}
foreach ( $context -> vars_in_scope as $var => $type ) {
2016-10-13 18:27:23 -04:00
if ( strpos ( $var , '$this->' ) !== 0 ) {
2016-08-13 23:26:45 -04:00
unset ( $context -> vars_in_scope [ $var ]);
}
}
foreach ( $context -> vars_possibly_in_scope as $var => $type ) {
2016-10-13 18:27:23 -04:00
if ( strpos ( $var , '$this->' ) !== 0 ) {
2016-08-13 23:26:45 -04:00
unset ( $context -> vars_possibly_in_scope [ $var ]);
}
}
2016-10-03 11:38:59 -04:00
if ( $hash && ClassLikeChecker :: getThisClass () && $this instanceof MethodChecker ) {
2016-08-13 23:26:45 -04:00
self :: $no_effects_hashes [ $hash ] = [ $context -> vars_in_scope , $context -> vars_possibly_in_scope ];
}
}
}
/**
* Adds return types for the given function
* @ param string $return_type
* @ param array < Type > $context -> vars_in_scope
* @ param array < bool > $context -> vars_possibly_in_scope
*/
public function addReturnTypes ( $return_type , Context $context )
{
if ( isset ( $this -> return_vars_in_scope [ $return_type ])) {
$this -> return_vars_in_scope [ $return_type ] = TypeChecker :: combineKeyedTypes ( $context -> vars_in_scope , $this -> return_vars_in_scope [ $return_type ]);
}
else {
$this -> return_vars_in_scope [ $return_type ] = $context -> vars_in_scope ;
}
if ( isset ( $this -> return_vars_possibly_in_scope [ $return_type ])) {
$this -> return_vars_possibly_in_scope [ $return_type ] = array_merge ( $context -> vars_possibly_in_scope , $this -> return_vars_possibly_in_scope [ $return_type ]);
}
else {
$this -> return_vars_possibly_in_scope [ $return_type ] = $context -> vars_possibly_in_scope ;
}
}
/**
* @ return null | string
*/
public function getMethodId ()
{
2016-10-15 00:12:57 -04:00
if ( $this -> function instanceof Function_ || $this -> function instanceof ClassMethod ) {
return ( $this instanceof MethodChecker ? $this -> absolute_class . '::' : '' ) . strtolower ( $this -> function -> name );
2016-08-13 23:26:45 -04:00
}
}
public function getNamespace ()
{
return $this -> namespace ;
}
2016-10-15 00:12:57 -04:00
/**
* @ return array < string >
*/
2016-08-13 23:26:45 -04:00
public function getAliasedClasses ()
{
2016-08-15 01:21:50 -04:00
return $this -> source -> getAliasedClasses ();
2016-08-13 23:26:45 -04:00
}
public function getAbsoluteClass ()
{
return $this -> absolute_class ;
}
public function getClassName ()
{
return $this -> class_name ;
}
public function getClassLikeChecker ()
{
return $this -> source -> getClassLikeChecker ();
}
2016-10-09 17:54:58 -04:00
/**
* @ return string | null
*/
2016-08-13 23:26:45 -04:00
public function getParentClass ()
{
return $this -> class_extends ;
}
public function getFileName ()
{
return $this -> file_name ;
}
2016-08-24 19:00:44 -04:00
public function getIncludeFileName ()
{
return $this -> include_file_name ;
}
2016-10-09 17:54:58 -04:00
/**
* @ param string | null $file_name
*/
2016-08-24 19:00:44 -04:00
public function setIncludeFileName ( $file_name )
{
$this -> include_file_name = $file_name ;
}
public function getCheckedFileName ()
{
return $this -> include_file_name ? : $this -> file_name ;
}
2016-08-13 23:26:45 -04:00
public function isStatic ()
{
return $this -> is_static ;
}
2016-10-15 00:12:57 -04:00
/**
* @ return StatementsSource
*/
2016-08-13 23:26:45 -04:00
public function getSource ()
{
return $this -> source ;
}
2016-10-09 17:54:58 -04:00
/**
* Get a list of suppressed issues
* @ return array < string >
*/
2016-08-13 23:26:45 -04:00
public function getSuppressedIssues ()
{
return $this -> suppressed_issues ;
}
/**
* @ return false | null
*/
public function checkReturnTypes ( $update_doc_comment = false )
{
2016-10-15 00:12:57 -04:00
if ( ! $this -> function -> getStmts ()) {
2016-08-13 23:26:45 -04:00
return ;
}
2016-10-15 00:12:57 -04:00
if ( $this -> function instanceof ClassMethod || $this -> function instanceof Function_ ) {
if ( $this -> function -> name === '__construct' ) {
// we know that constructors always return this
return ;
}
2016-08-13 23:26:45 -04:00
}
2016-10-15 00:12:57 -04:00
$method_id = ( string ) $this -> getMethodId ();
2016-08-13 23:26:45 -04:00
2016-08-15 15:14:28 -04:00
if ( $this instanceof MethodChecker ) {
$method_return_types = MethodChecker :: getMethodReturnTypes ( $method_id );
2016-08-13 23:26:45 -04:00
}
else {
2016-08-14 13:13:53 -04:00
$method_return_types = FunctionChecker :: getFunctionReturnTypes ( $method_id , $this -> file_name );
2016-08-13 23:26:45 -04:00
}
if ( ! $method_return_types ) {
return ;
}
// passing it through fleshOutTypes eradicates errant $ vars
$declared_return_type = StatementsChecker :: fleshOutTypes (
$method_return_types ,
[],
$this -> absolute_class ,
$method_id
);
if ( $declared_return_type ) {
2016-10-15 00:12:57 -04:00
$inferred_return_types = \Psalm\EffectsAnalyser :: getReturnTypes ( $this -> function -> getStmts (), true );
2016-08-13 23:26:45 -04:00
if ( ! $inferred_return_types ) {
if ( $declared_return_type -> isVoid ()) {
return ;
}
2016-10-15 00:12:57 -04:00
if ( ScopeChecker :: onlyThrows ( $this -> function -> getStmts ())) {
2016-08-13 23:26:45 -04:00
// if there's a single throw statement, it's presumably an exception saying this method is not to be used
return ;
}
if ( IssueBuffer :: accepts (
new InvalidReturnType (
2016-08-15 15:14:28 -04:00
'No return type was found for method ' . MethodChecker :: getCasedMethodId ( $method_id ) . ' but return type \'' . $declared_return_type . '\' was expected' ,
2016-08-24 19:00:44 -04:00
$this -> getCheckedFileName (),
2016-08-13 23:26:45 -04:00
$this -> function -> getLine ()
)
)) {
return false ;
}
return ;
}
$inferred_return_type = Type :: combineTypes ( $inferred_return_types );
if ( $inferred_return_type && ! $inferred_return_type -> isMixed () && ! $declared_return_type -> isMixed ()) {
if ( $inferred_return_type -> isNull () && $declared_return_type -> isVoid ()) {
return ;
}
if ( ! TypeChecker :: hasIdenticalTypes ( $declared_return_type , $inferred_return_type , $this -> absolute_class )) {
if ( IssueBuffer :: accepts (
new InvalidReturnType (
2016-08-15 15:14:28 -04:00
'The given return type \'' . $declared_return_type . '\' for ' . MethodChecker :: getCasedMethodId ( $method_id ) . ' is incorrect, got \'' . $inferred_return_type . '\'' ,
2016-08-24 19:00:44 -04:00
$this -> getCheckedFileName (),
2016-08-13 23:26:45 -04:00
$this -> function -> getLine ()
),
$this -> getSuppressedIssues ()
)) {
return false ;
}
}
}
return ;
}
}
2016-10-15 00:12:57 -04:00
/**
* @ param array < object - like { type : string , name : string } > $docblock_params
* @ param array < string , Type\Union > $function_param_names
* @ param array < \Psalm\FunctionLikeParameter > & $function_signature
* @ param int $method_line_number
* @ return false | null
*/
2016-08-13 23:26:45 -04:00
protected function improveParamsFromDocblock ( array $docblock_params , array $function_param_names , array & $function_signature , $method_line_number )
{
foreach ( $docblock_params as $docblock_param ) {
$param_name = $docblock_param [ 'name' ];
if ( ! array_key_exists ( $param_name , $function_param_names )) {
if ( IssueBuffer :: accepts (
new InvalidDocblock (
'Parameter $' . $param_name . ' does not appear in the argument list for ' . $this -> getMethodId (),
2016-08-24 19:00:44 -04:00
$this -> getCheckedFileName (),
2016-08-13 23:26:45 -04:00
$method_line_number
)
)) {
return false ;
}
continue ;
}
$new_param_type =
Type :: parseString (
self :: fixUpLocalType (
$docblock_param [ 'type' ],
null ,
$this -> namespace ,
2016-08-15 01:21:50 -04:00
$this -> getAliasedClasses ()
2016-08-13 23:26:45 -04:00
)
);
if ( $function_param_names [ $param_name ] && ! $function_param_names [ $param_name ] -> isMixed ()) {
if ( ! $new_param_type -> isIn ( $function_param_names [ $param_name ])) {
if ( IssueBuffer :: accepts (
new InvalidDocblock (
'Parameter $' . $param_name . ' has wrong type \'' . $new_param_type . '\', should be \'' . $function_param_names [ $param_name ] . '\'' ,
2016-08-24 19:00:44 -04:00
$this -> getCheckedFileName (),
2016-08-13 23:26:45 -04:00
$method_line_number
)
)) {
return false ;
}
continue ;
}
}
foreach ( $function_signature as & $function_signature_param ) {
2016-10-09 17:54:58 -04:00
if ( $function_signature_param -> name === $param_name ) {
$existing_param_type_nullable = $function_signature_param -> is_nullable ;
2016-08-13 23:26:45 -04:00
if ( $existing_param_type_nullable && ! $new_param_type -> isNullable ()) {
2016-09-09 16:21:49 -04:00
$new_param_type -> types [ 'null' ] = new Type\Atomic ( 'null' );
2016-08-13 23:26:45 -04:00
}
2016-10-15 18:01:04 -04:00
$function_signature_param -> signature_type = $function_signature_param -> type ;
2016-10-09 17:54:58 -04:00
$function_signature_param -> type = $new_param_type ;
2016-08-13 23:26:45 -04:00
break ;
}
}
}
}
2016-10-09 17:54:58 -04:00
/**
* @ param PhpParser\Node\Param $param
2016-10-15 18:01:04 -04:00
* @ param string $absolute_class
* @ param string $namespace
* @ param array < string > $aliased_classes
2016-10-09 17:54:58 -04:00
* @ return FunctionLikeParameter
*/
2016-10-15 18:01:04 -04:00
protected static function getParamArray ( PhpParser\Node\Param $param , $absolute_class , $namespace , array $aliased_classes )
2016-08-13 23:26:45 -04:00
{
$param_type = null ;
$is_nullable = $param -> default !== null &&
$param -> default instanceof \PhpParser\Node\Expr\ConstFetch &&
$param -> default -> name instanceof PhpParser\Node\Name &&
$param -> default -> name -> parts === [ 'null' ];
if ( $param -> type ) {
if ( is_string ( $param -> type )) {
$param_type_string = $param -> type ;
}
elseif ( $param -> type instanceof PhpParser\Node\Name\FullyQualified ) {
$param_type_string = implode ( '\\' , $param -> type -> parts );
}
elseif ( $param -> type -> parts === [ 'self' ]) {
2016-10-15 18:01:04 -04:00
$param_type_string = $absolute_class ;
2016-08-13 23:26:45 -04:00
}
else {
2016-08-15 01:21:50 -04:00
$param_type_string = ClassLikeChecker :: getAbsoluteClassFromString (
implode ( '\\' , $param -> type -> parts ),
2016-10-15 18:01:04 -04:00
$namespace ,
$aliased_classes
2016-08-15 01:21:50 -04:00
);
2016-08-13 23:26:45 -04:00
}
if ( $param_type_string ) {
if ( $is_nullable ) {
$param_type_string .= '|null' ;
}
$param_type = Type :: parseString ( $param_type_string );
}
}
$is_optional = $param -> default !== null ;
2016-10-09 17:54:58 -04:00
return new FunctionLikeParameter (
$param -> name ,
$param -> byRef ,
$param_type ? : Type :: getMixed (),
$is_optional ,
2016-10-15 00:12:57 -04:00
$is_nullable ,
$param -> getLine ()
2016-10-09 17:54:58 -04:00
);
2016-08-13 23:26:45 -04:00
}
2016-10-15 00:12:57 -04:00
/**
* @ param \ReflectionParameter $param
* @ return FunctionLikeParameter
*/
2016-08-14 13:13:53 -04:00
protected static function getReflectionParamArray ( \ReflectionParameter $param )
2016-08-13 23:26:45 -04:00
{
$param_type_string = null ;
if ( $param -> isArray ()) {
$param_type_string = 'array' ;
}
else {
$param_class = null ;
try {
2016-10-15 00:12:57 -04:00
/** @var \ReflectionClass */
2016-08-13 23:26:45 -04:00
$param_class = $param -> getClass ();
}
catch ( \ReflectionException $e ) {
// do nothing
}
if ( $param_class ) {
2016-10-15 00:12:57 -04:00
$param_type_string = ( string ) $param_class -> getName ();
2016-08-13 23:26:45 -04:00
}
}
$is_nullable = false ;
2016-10-15 00:12:57 -04:00
$is_optional = ( bool ) $param -> isOptional ();
2016-08-13 23:26:45 -04:00
try {
$is_nullable = $param -> getDefaultValue () === null ;
if ( $param_type_string && $is_nullable ) {
$param_type_string .= '|null' ;
}
}
catch ( \ReflectionException $e ) {
// do nothing
}
2016-10-15 00:12:57 -04:00
$param_name = ( string ) $param -> getName ();
2016-08-13 23:26:45 -04:00
$param_type = $param_type_string ? Type :: parseString ( $param_type_string ) : Type :: getMixed ();
2016-10-09 17:54:58 -04:00
return new FunctionLikeParameter (
$param_name ,
2016-10-15 00:12:57 -04:00
( bool ) $param -> isPassedByReference (),
2016-10-09 17:54:58 -04:00
$param_type ,
2016-10-10 01:35:12 -04:00
$is_optional ,
$is_nullable
2016-10-09 17:54:58 -04:00
);
2016-08-13 23:26:45 -04:00
}
2016-10-14 00:53:43 -04:00
/**
2016-10-15 00:12:57 -04:00
* @ param string $return_type
* @ param string | null $absolute_class
* @ param string $namespace
* @ param array $aliased_classes
2016-10-14 00:53:43 -04:00
* @ return string
*/
public static function fixUpLocalType ( $return_type , $absolute_class , $namespace , array $aliased_classes )
2016-08-13 23:26:45 -04:00
{
if ( strpos ( $return_type , '[' ) !== false ) {
$return_type = Type :: convertSquareBrackets ( $return_type );
}
$return_type_tokens = Type :: tokenize ( $return_type );
2016-10-03 16:39:42 -04:00
foreach ( $return_type_tokens as $i => & $return_type_token ) {
2016-08-13 23:26:45 -04:00
if ( $return_type_token [ 0 ] === '\\' ) {
$return_type_token = substr ( $return_type_token , 1 );
continue ;
}
2016-10-03 16:39:42 -04:00
if ( in_array ( $return_type_token , [ '<' , '>' , '|' , '?' , ',' , '{' , '}' , ':' ])) {
continue ;
}
if ( isset ( $return_type_token [ $i + 1 ]) && $return_type_token [ $i + 1 ] === ':' ) {
2016-08-13 23:26:45 -04:00
continue ;
}
$return_type_token = Type :: fixScalarTerms ( $return_type_token );
if ( $return_type_token [ 0 ] === strtoupper ( $return_type_token [ 0 ])) {
2016-09-02 13:47:11 -04:00
if ( $return_type_token [ 0 ] === '$' ) {
if ( $return_type === '$this' ) {
$return_type_token = 'static' ;
}
2016-08-13 23:26:45 -04:00
continue ;
}
$return_type_token = ClassLikeChecker :: getAbsoluteClassFromString ( $return_type_token , $namespace , $aliased_classes );
}
}
return implode ( '' , $return_type_tokens );
}
2016-08-22 15:00:12 -04:00
/**
* Does the input param type match the given param type
* @ param Type\Union $input_type
* @ param Type\Union $param_type
* @ param bool & $has_scalar_match
2016-10-09 17:54:58 -04:00
* @ param bool & $type_coerced whether or not there was type coercion involved
2016-08-22 15:00:12 -04:00
* @ return bool
*/
2016-10-09 17:54:58 -04:00
public static function doesParamMatch ( Type\Union $input_type , Type\Union $param_type , & $has_scalar_match = null , & $type_coerced = null )
2016-08-22 15:00:12 -04:00
{
$has_scalar_match = true ;
if ( $param_type -> isMixed ()) {
return true ;
}
$type_match_found = false ;
$has_type_mismatch = false ;
foreach ( $input_type -> types as $input_type_part ) {
if ( $input_type_part -> isNull ()) {
continue ;
}
$type_match_found = false ;
$scalar_type_match_found = false ;
foreach ( $param_type -> types as $param_type_part ) {
if ( $param_type_part -> isNull ()) {
continue ;
}
if ( $input_type_part -> value === $param_type_part -> value ||
ClassChecker :: classExtendsOrImplements ( $input_type_part -> value , $param_type_part -> value ) ||
StatementsChecker :: isMock ( $input_type_part -> value )
) {
$type_match_found = true ;
break ;
}
if ( $input_type_part -> value === 'false' && $param_type_part -> value === 'bool' ) {
$type_match_found = true ;
}
if ( $input_type_part -> value === 'int' && $param_type_part -> value === 'float' ) {
$type_match_found = true ;
}
2016-08-24 17:06:41 -04:00
if ( $input_type_part -> value === 'Closure' && $param_type_part -> value === 'callable' ) {
$type_match_found = true ;
}
2016-08-22 15:00:12 -04:00
if ( $param_type_part -> isNumeric () && $input_type_part -> isNumericType ()) {
$type_match_found = true ;
}
2016-09-21 16:42:39 -04:00
if ( $param_type_part -> isArray () && $input_type_part -> isObjectLike ()) {
$type_match_found = true ;
}
2016-08-30 00:39:17 -04:00
if ( $param_type_part -> isScalar () && $input_type_part -> isScalarType ()) {
$type_match_found = true ;
}
2016-08-22 15:00:12 -04:00
if ( $param_type_part -> isCallable () && ( $input_type_part -> value === 'string' || $input_type_part -> value === 'array' )) {
// @todo add value checks if possible here
$type_match_found = true ;
}
2016-08-30 00:39:17 -04:00
if ( $input_type_part -> isNumeric ()) {
if ( $param_type_part -> isNumericType ()) {
$scalar_type_match_found = true ;
}
}
2016-08-31 13:25:29 -04:00
if ( $input_type_part -> isScalarType () || $input_type_part -> isScalar ()) {
2016-08-22 15:00:12 -04:00
if ( $param_type_part -> isScalarType ()) {
$scalar_type_match_found = true ;
}
}
2016-08-30 00:39:17 -04:00
else if ( $param_type_part -> isObject () && ! $input_type_part -> isArray () && ! $input_type_part -> isResource ()) {
2016-08-22 15:00:12 -04:00
$type_match_found = true ;
}
if ( ClassChecker :: classExtendsOrImplements ( $param_type_part -> value , $input_type_part -> value )) {
2016-10-09 17:54:58 -04:00
$type_coerced = true ;
2016-08-22 15:00:12 -04:00
$type_match_found = true ;
break ;
}
}
if ( ! $type_match_found ) {
if ( ! $scalar_type_match_found ) {
$has_scalar_match = false ;
}
return false ;
}
}
return true ;
}
2016-10-09 17:54:58 -04:00
/**
* @ param string $method_id
* @ param array < PhpParser\Node\Arg > $args
* @ param string $file_name
* @ return array < int , FunctionLikeParameter >
*/
2016-08-22 23:02:03 -04:00
public static function getParamsById ( $method_id , array $args , $file_name )
2016-08-22 15:00:12 -04:00
{
if ( strpos ( $method_id , '::' )) {
return MethodChecker :: getMethodParams ( $method_id );
}
2016-10-15 00:12:57 -04:00
$function_param_options = FunctionChecker :: getParamsFromCallMap ( $method_id );
2016-08-22 15:00:12 -04:00
if ( $function_param_options === null ) {
2016-08-22 15:42:05 -04:00
return FunctionChecker :: getParams ( strtolower ( $method_id ), $file_name );
2016-08-22 15:00:12 -04:00
}
$function_params = null ;
if ( count ( $function_param_options ) === 1 ) {
return $function_param_options [ 0 ];
}
foreach ( $function_param_options as $possible_function_params ) {
$all_args_match = true ;
foreach ( $args as $argument_offset => $arg ) {
2016-08-22 15:43:08 -04:00
if ( count ( $possible_function_params ) <= $argument_offset ) {
2016-08-22 15:00:12 -04:00
break ;
}
2016-10-09 17:54:58 -04:00
$param_type = $possible_function_params [ $argument_offset ] -> type ;
2016-08-22 15:00:12 -04:00
if ( ! isset ( $arg -> value -> inferredType )) {
continue ;
}
2016-10-10 22:49:43 -04:00
if ( $arg -> value -> inferredType -> isMixed ()) {
continue ;
}
if ( FunctionLikeChecker :: doesParamMatch ( $arg -> value -> inferredType , $param_type )) {
continue ;
2016-08-22 15:00:12 -04:00
}
$all_args_match = false ;
break ;
}
if ( $all_args_match ) {
return $possible_function_params ;
}
}
// if we don't succeed in finding a match, set to the first possible and wait for issues below
return $function_param_options [ 0 ];
}
2016-08-13 23:26:45 -04:00
}