/** * Visit a node with kind `\ast\AST_METHOD` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitMethod(Decl $node) : Context { // Bomb out if we're not in a class context $clazz = $this->getContextClass(); $method_name = (string) $node->name; $method_fqsen = FullyQualifiedMethodName::fromStringInContext($method_name, $this->context); // Hunt for an available alternate ID if necessary $alternate_id = 0; while ($this->code_base->hasMethodWithFQSEN($method_fqsen)) { $method_fqsen = $method_fqsen->withAlternateId(++$alternate_id); } // Create a new context with a new scope $context = $this->context->withScope(new Scope()); $method = Method::fromNode($context, $this->code_base, $node); // Override the FQSEN with the found alternate ID $method->setFQSEN($method_fqsen); $clazz->addMethod($this->code_base, $method); if ('__construct' === $method_name) { $clazz->setIsParentConstructorCalled(false); } elseif ('__invoke' === $method_name) { $clazz->getUnionType()->addType(CallableType::instance()); } elseif ('__toString' === $method_name && !$this->context->getIsStrictTypes()) { $clazz->getUnionType()->addType(StringType::instance()); } // Send the context into the method and reset the scope $context = $this->context->withMethodFQSEN($method->getFQSEN()); return $context; }
/** * @param string $string * A string representing a type * * @param Context $context * The context in which the type string was * found * * @return Type * Parse a type from the given string */ public static function fromStringInContext(string $string, Context $context) : Type { assert($string !== '', "Type cannot be empty in {$context}"); $namespace = null; // Extract the namespace if the type string is // fully-qualified if ('\\' === $string[0]) { list($namespace, $string) = self::namespaceAndTypeFromString($string); } $type_name = $string; // @var bool // True if this type name if of the form 'C[]' $is_generic_array_type = self::isGenericArrayString($type_name); // If this is a generic array type, get the name of // the type of each element $non_generic_array_type_name = $type_name; if ($is_generic_array_type && false !== ($pos = strpos($type_name, '[]'))) { $non_generic_array_type_name = substr($type_name, 0, $pos); } // Check to see if the type name is mapped via // a using clause. // // Gotta check this before checking for native types // because there are monsters out there that will // remap the names via things like `use \Foo\String`. if ($context->hasNamespaceMapFor(T_CLASS, $non_generic_array_type_name)) { $fqsen = $context->getNamespaceMapFor(T_CLASS, $non_generic_array_type_name); if ($is_generic_array_type) { return GenericArrayType::fromElementType(Type::make($fqsen->getNamespace(), $fqsen->getName())); } return Type::make($fqsen->getNamespace(), $fqsen->getName()); } // If this was a fully qualified type, we're all // set if (!empty($namespace)) { return self::fromNamespaceAndName($namespace, $type_name); } if ($is_generic_array_type && self::isNativeTypeString($type_name)) { return self::fromInternalTypeName($type_name); } else { // Check to see if its a builtin type switch (self::canonicalNameFromName($type_name)) { case 'array': return \Phan\Language\Type\ArrayType::instance(); case 'bool': return \Phan\Language\Type\BoolType::instance(); case 'callable': return \Phan\Language\Type\CallableType::instance(); case 'float': return \Phan\Language\Type\FloatType::instance(); case 'int': return \Phan\Language\Type\IntType::instance(); case 'mixed': return \Phan\Language\Type\MixedType::instance(); case 'null': return \Phan\Language\Type\NullType::instance(); case 'object': return \Phan\Language\Type\ObjectType::instance(); case 'resource': return \Phan\Language\Type\ResourceType::instance(); case 'string': return \Phan\Language\Type\StringType::instance(); case 'void': return \Phan\Language\Type\VoidType::instance(); } } // Things like `self[]` or `$this[]` if ($is_generic_array_type && self::isSelfTypeString($non_generic_array_type_name) && $context->isInClassScope()) { // Callers of this method should be checking on their own // to see if this type is a reference to 'parent' and // dealing with it there. We don't want to have this // method be dependent on the code base assert('parent' !== $non_generic_array_type_name, __METHOD__ . " does not know how to handle the type name 'parent' in {$context}"); return GenericArrayType::fromElementType(static::fromFullyQualifiedString((string) $context->getClassFQSEN())); } // If this is a type referencing the current class // in scope such as 'self' or 'static', return that. if (self::isSelfTypeString($type_name) && $context->isInClassScope()) { // Callers of this method should be checking on their own // to see if this type is a reference to 'parent' and // dealing with it there. We don't want to have this // method be dependent on the code base assert('parent' !== $type_name, __METHOD__ . " does not know how to handle the type name 'parent' in {$context}"); return static::fromFullyQualifiedString((string) $context->getClassFQSEN()); } // Attach the context's namespace to the type name return self::fromNamespaceAndName($context->getNamespace() ?: '\\', $type_name); }
/** * Visit a node with kind `\ast\AST_METHOD` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitMethod(Decl $node) : Context { // Bomb out if we're not in a class context $class = $this->getContextClass(); $method_name = (string) $node->name; $method_fqsen = FullyQualifiedMethodName::fromStringInContext($method_name, $this->context); // Hunt for an available alternate ID if necessary $alternate_id = 0; while ($this->code_base->hasMethodWithFQSEN($method_fqsen)) { $method_fqsen = $method_fqsen->withAlternateId(++$alternate_id); } $method = Method::fromNode(clone $this->context, $this->code_base, $node, $method_fqsen); $class->addMethod($this->code_base, $method, new None()); if ('__construct' === $method_name) { $class->setIsParentConstructorCalled(false); if ($class->isGeneric()) { // Get the set of template type identifiers defined on // the class $template_type_identifiers = array_keys($class->getTemplateTypeMap()); // Get the set of template type identifiers defined // across all parameter types $parameter_template_type_identifiers = []; foreach ($method->getParameterList() as $parameter) { foreach ($parameter->getUnionType()->getTypeSet() as $type) { if ($type instanceof TemplateType) { $parameter_template_type_identifiers[] = $type->getName(); } } } $missing_template_type_identifiers = array_diff($template_type_identifiers, $parameter_template_type_identifiers); if ($missing_template_type_identifiers) { $this->emitIssue(Issue::GenericConstructorTypes, $node->lineno ?? 0, implode(',', $missing_template_type_identifiers), (string) $class->getFQSEN()); } } } elseif ('__invoke' === $method_name) { $class->getUnionType()->addType(CallableType::instance()); } elseif ('__toString' === $method_name && !$this->context->getIsStrictTypes()) { $class->getUnionType()->addType(StringType::instance()); } // Create a new context with a new scope return $this->context->withScope($method->getInternalScope()); }
/** * Visit a node with kind `\ast\AST_CLOSURE` * * @param Node $node * A node of the type indicated by the method name that we'd * like to figure out the type that it produces. * * @return UnionType * The set of types that are possibly produced by the * given node */ public function visitClosure(Decl $node) : UnionType { // The type of a closure is the fqsen pointing // at its definition $closure_fqsen = FullyQualifiedFunctionName::fromClosureInContext($this->context); $type = CallableType::instanceWithClosureFQSEN($closure_fqsen)->asUnionType(); return $type; }
/** * Check to see if the given Clazz is a duplicate * * @param Method $method * The method we're analyzing arguments for * * @param Node $node * The node holding the method call we're looking at * * @param Context $context * The context in which we see the call * * @param CodeBase $code_base * * @return null * * @see \Phan\Deprecated\Pass2::arg_check * Formerly `function arg_check` */ private static function analyzeInternalArgumentType(Method $method, Node $node, Context $context, CodeBase $code_base) { $arglist = $node->children['args']; $argcount = count($arglist->children); switch ($method->getName()) { case 'join': case 'implode': // (string glue, array pieces), // (array pieces, string glue) or // (array pieces) if ($argcount == 1) { self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#1(pieces) is %s but {$method->getFQSEN()}() takes array when passed only 1 arg"); return; } else { if ($argcount == 2) { $arg1_type = UnionType::fromNode($context, $code_base, $arglist->children[0]); $arg2_type = UnionType::fromNode($context, $code_base, $arglist->children[1]); if ((string) $arg1_type == 'array') { if (!$arg1_type->canCastToUnionType(StringType::instance()->asUnionType())) { Log::err(Log::EPARAM, "arg#2(glue) is {$arg2_type} but {$method->getFQSEN()}() takes string when arg#1 is array", $context->getFile(), $context->getLineNumberStart()); } } else { if ((string) $arg1_type == 'string') { if (!$arg2_type->canCastToUnionType(ArrayType::instance()->asUnionType())) { Log::err(Log::EPARAM, "arg#2(pieces) is {$arg2_type} but {$method->getFQSEN()}() takes array when arg#1 is string", $context->getFile(), $context->getLineNumberStart()); } } } return; } } // Any other arg counts we will let the regular // checks handle break; case 'array_udiff': case 'array_diff_uassoc': case 'array_uintersect_assoc': case 'array_intersect_ukey': if ($argcount < 3) { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$method->getFQSEN()}() which requires {$method->getNumberOfRequiredParameters()} arg(s)", $context->getFile(), $context->getLineNumberStart()); return; } self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), "The last argument to {$method->getFQSEN()} must be a callable"); for ($i = 0; $i < $argcount - 1; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, CallableType::instance()->asUnionType(), "arg#" . ($i + 1) . " is %s but {$method->getFQSEN()}() takes array"); } return; case 'array_diff_uassoc': case 'array_uintersect_uassoc': if ($argcount < 4) { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$method->getFQSEN()}() which requires {$method->getNumberOfRequiredParameters()} arg(s)", $context->getFile(), $context->getLineNumberStart()); return; } // The last 2 arguments must be a callable and there // can be a variable number of arrays before it self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), "The last argument to {$method->getFQSEN()} must be a callable"); self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 2], $context, $code_base, CallableType::instance()->asUnionType(), "The second last argument to {$method->getFQSEN()} must be a callable"); for ($i = 0; $i < $argcount - 2; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#" . ($i + 1) . " is %s but {$method->getFQSEN()}() takes array"); } return; case 'strtok': // (string str, string token) or (string token) if ($argcount == 1) { // If we have just one arg it must be a string token self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#1(token) is %s but {$method->getFQSEN()}() takes string when passed only one arg"); } // The arginfo check will handle the other case break; case 'min': case 'max': if ($argcount == 1) { // If we have just one arg it must be an array if (!self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#1(values) is %s but {$method->getFQSEN()}() takes array when passed only one arg")) { return; } } // The arginfo check will handle the other case break; default: break; } }
/** * @param string $string * A string representing a type * * @param Context $context * The context in which the type string was * found * * @return Type * Parse a type from the given string */ public static function fromStringInContext(string $string, Context $context) : Type { assert($string !== '', "Type cannot be empty"); // Extract the namespace, type and parameter type name list $tuple = self::typeStringComponents($string); $namespace = $tuple->_0; $type_name = $tuple->_1; $template_parameter_type_name_list = $tuple->_2; // Map the names of the types to actual types in the // template parameter type list $template_parameter_type_list = array_map(function (string $type_name) use($context) { return Type::fromStringInContext($type_name, $context)->asUnionType(); }, $template_parameter_type_name_list); // @var bool // True if this type name if of the form 'C[]' $is_generic_array_type = self::isGenericArrayString($type_name); // If this is a generic array type, get the name of // the type of each element $non_generic_array_type_name = $type_name; if ($is_generic_array_type && false !== ($pos = strrpos($type_name, '[]'))) { $non_generic_array_type_name = substr($type_name, 0, $pos); } // Check to see if the type name is mapped via // a using clause. // // Gotta check this before checking for native types // because there are monsters out there that will // remap the names via things like `use \Foo\String`. $non_generic_partially_qualified_array_type_name = $non_generic_array_type_name; if ($namespace) { $non_generic_partially_qualified_array_type_name = $namespace . '\\' . $non_generic_partially_qualified_array_type_name; } if ($context->hasNamespaceMapFor(\ast\flags\USE_NORMAL, $non_generic_partially_qualified_array_type_name)) { $fqsen = $context->getNamespaceMapFor(\ast\flags\USE_NORMAL, $non_generic_partially_qualified_array_type_name); if ($is_generic_array_type) { return GenericArrayType::fromElementType(Type::make($fqsen->getNamespace(), $fqsen->getName(), $template_parameter_type_list)); } return Type::make($fqsen->getNamespace(), $fqsen->getName(), $template_parameter_type_list); } // If this was a fully qualified type, we're all // set if (!empty($namespace) && 0 === strpos($namespace, '\\')) { return self::make($namespace, $type_name, $template_parameter_type_list); } if ($is_generic_array_type && self::isNativeTypeString($type_name)) { return self::fromInternalTypeName($type_name); } else { // Check to see if its a builtin type switch (strtolower(self::canonicalNameFromName($type_name))) { case 'array': return \Phan\Language\Type\ArrayType::instance(); case 'bool': return \Phan\Language\Type\BoolType::instance(); case 'callable': return \Phan\Language\Type\CallableType::instance(); case 'float': return \Phan\Language\Type\FloatType::instance(); case 'int': return \Phan\Language\Type\IntType::instance(); case 'mixed': return \Phan\Language\Type\MixedType::instance(); case 'null': return \Phan\Language\Type\NullType::instance(); case 'object': return \Phan\Language\Type\ObjectType::instance(); case 'resource': return \Phan\Language\Type\ResourceType::instance(); case 'string': return \Phan\Language\Type\StringType::instance(); case 'void': return \Phan\Language\Type\VoidType::instance(); case 'static': return \Phan\Language\Type\StaticType::instance(); } } // Things like `self[]` or `$this[]` if ($is_generic_array_type && self::isSelfTypeString($non_generic_array_type_name) && $context->isInClassScope()) { // Callers of this method should be checking on their own // to see if this type is a reference to 'parent' and // dealing with it there. We don't want to have this // method be dependent on the code base assert('parent' !== $non_generic_array_type_name, __METHOD__ . " does not know how to handle the type name 'parent'"); return GenericArrayType::fromElementType(static::fromFullyQualifiedString((string) $context->getClassFQSEN())); } // If this is a type referencing the current class // in scope such as 'self' or 'static', return that. if (self::isSelfTypeString($type_name) && $context->isInClassScope()) { // Callers of this method should be checking on their own // to see if this type is a reference to 'parent' and // dealing with it there. We don't want to have this // method be dependent on the code base assert('parent' !== $type_name, __METHOD__ . " does not know how to handle the type name 'parent'"); return static::fromFullyQualifiedString((string) $context->getClassFQSEN()); } // Merge the current namespace with the given relative // namespace if (!empty($context->getNamespace()) && !empty($namespace)) { $namespace = $context->getNamespace() . '\\' . $namespace; } else { if (!empty($context->getNamespace())) { $namespace = $context->getNamespace(); } else { $namespace = '\\' . $namespace; } } // Attach the context's namespace to the type name return self::make($namespace, $type_name, $template_parameter_type_list); }
/** * @param string $string * A string representing a type * * @param Context $context * The context in which the type string was * found * * @return Type * Parse a type from the given string */ public static function fromStringInContext(string $string, Context $context) : Type { assert($string !== '', "Type cannot be empty in {$context}"); $namespace = null; // Extract the namespace if the type string is // fully-qualified if ('\\' === $string[0]) { list($namespace, $string) = self::namespaceAndTypeFromString($string); } $type_name = strtolower($string); // Check to see if the type name is mapped via // a using clause. // // Gotta check this before checking for native types // because there are monsters out there that will // remap the names via things like `use \Foo\String`. if ($context->hasNamespaceMapFor(T_CLASS, $type_name)) { $fqsen = $context->getNamespaceMapFor(T_CLASS, $type_name); return new Type($fqsen->getNamespace(), $fqsen->getName()); } // If this was a fully qualified type, we're all // set if (!empty($namespace)) { return self::fromNamespaceAndName($namespace, $type_name); } // Check to see if its a builtin type switch (self::canonicalNameFromName($type_name)) { case 'array': return \Phan\Language\Type\ArrayType::instance(); case 'bool': return \Phan\Language\Type\BoolType::instance(); case 'callable': return \Phan\Language\Type\CallableType::instance(); case 'float': return \Phan\Language\Type\FloatType::instance(); case 'int': return \Phan\Language\Type\IntType::instance(); case 'mixed': return \Phan\Language\Type\MixedType::instance(); case 'null': return \Phan\Language\Type\NullType::instance(); case 'object': return \Phan\Language\Type\ObjectType::instance(); case 'resource': return \Phan\Language\Type\ResourceType::instance(); case 'string': return \Phan\Language\Type\StringType::instance(); case 'void': return \Phan\Language\Type\VoidType::instance(); } // If this is a type referencing the current class // in scope such as 'self' or 'static', return that. if (self::isSelfTypeString($type_name) && $context->isInClassScope()) { return static::fromFullyQualifiedString((string) $context->getClassFQSEN()); } // Attach the context's namespace to the type name return self::fromNamespaceAndName($context->getNamespace() ?: '\\', $type_name); }
/** * Check to see if the given Clazz is a duplicate * * @param Method $method * The method we're analyzing arguments for * * @param Node $node * The node holding the method call we're looking at * * @param Context $context * The context in which we see the call * * @param CodeBase $code_base * * @return null * * @see \Phan\Deprecated\Pass2::arg_check * Formerly `function arg_check` */ private static function analyzeInternalArgumentType(Method $method, Node $node, Context $context, CodeBase $code_base) { $arglist = $node->children['args']; $argcount = count($arglist->children); switch ($method->getName()) { case 'join': case 'implode': // (string glue, array pieces), // (array pieces, string glue) or // (array pieces) if ($argcount == 1) { self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "arg#1(pieces) is %s but {$method->getFQSEN()}() takes array when passed only 1 arg" return Issue::fromType(Issue::ParamSpecial2)($context->getFile(), $context->getLineNumberStart(), [1, 'pieces', (string) $method->getFQSEN(), 'string', 'array']); }); return; } else { if ($argcount == 2) { $arg1_type = UnionType::fromNode($context, $code_base, $arglist->children[0]); $arg2_type = UnionType::fromNode($context, $code_base, $arglist->children[1]); if ((string) $arg1_type == 'array') { if (!$arg1_type->canCastToUnionType(StringType::instance()->asUnionType())) { Issue::emit(Issue::ParamSpecial1, $context->getFile(), $context->getLineNumberStart(), 2, 'glue', (string) $arg2_type, (string) $method->getFQSEN(), 'string', 1, 'array'); } } else { if ((string) $arg1_type == 'string') { if (!$arg2_type->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::emit(Issue::ParamSpecial1, $context->getFile(), $context->getLineNumberStart(), 2, 'pieces', (string) $arg2_type, (string) $method->getFQSEN(), 'array', 1, 'string'); } } } return; } } // Any other arg counts we will let the regular // checks handle break; case 'array_udiff': case 'array_diff_uassoc': case 'array_uintersect_assoc': case 'array_intersect_ukey': if ($argcount < 3) { Issue::emit(Issue::ParamTooFewInternal, $context->getFile(), $context->getLineNumberStart(), $argcount, (string) $method->getFQSEN(), $method->getNumberOfRequiredParameters()); return; } self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "The last argument to {$method->getFQSEN()} must be a callable" return Issue::fromType(Issue::ParamSpecial3)($context->getFile(), $context->getLineNumberStart(), [(string) $method->getFQSEN(), 'callable']); }); for ($i = 0; $i < $argcount - 1; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, CallableType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method, $i) { // "arg#".($i+1)." is %s but {$method->getFQSEN()}() takes array" return Issue::fromType(Issue::ParamTypeMismatch)($context->getFile(), $context->getLineNumberStart(), [$i + 1, (string) $node_type, (string) $method->getFQSEN(), 'array']); }); } return; case 'array_diff_uassoc': case 'array_uintersect_uassoc': if ($argcount < 4) { Issue::emit(Issue::ParamTooFewInternal, $context->getFile(), $context->getLineNumberStart(), $argcount, (string) $method->getFQSEN(), $method->getNumberOfRequiredParameters()); return; } // The last 2 arguments must be a callable and there // can be a variable number of arrays before it self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "The last argument to {$method->getFQSEN()} must be a callable" return Issue::fromType(Issue::ParamSpecial3)($context->getFile(), $context->getLineNumberStart(), [(string) $method->getFQSEN(), 'callable']); }); self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 2], $context, $code_base, CallableType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "The second last argument to {$method->getFQSEN()} must be a callable" return Issue::fromType(Issue::ParamSpecial4)($context->getFile(), $context->getLineNumberStart(), [(string) $method->getFQSEN(), 'callable']); }); for ($i = 0; $i < $argcount - 2; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, ArrayType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method, $i) { // "arg#".($i+1)." is %s but {$method->getFQSEN()}() takes array" return Issue::fromType(Issue::ParamTypeMismatch)($context->getFile(), $context->getLineNumberStart(), [$i + 1, (string) $node_type, (string) $method->getFQSEN(), 'array']); }); } return; case 'strtok': // (string str, string token) or (string token) if ($argcount == 1) { // If we have just one arg it must be a string token self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "arg#1(token) is %s but {$method->getFQSEN()}() takes string when passed only one arg" return Issue::fromType(Issue::ParamSpecial2)($context->getFile(), $context->getLineNumberStart(), [1, 'token', (string) $node_type, (string) $method->getFQSEN(), 'string']); }); } // The arginfo check will handle the other case break; case 'min': case 'max': if ($argcount == 1) { // If we have just one arg it must be an array if (!self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "arg#1(values) is %s but {$method->getFQSEN()}() takes array when passed only one arg" return Issue::fromType(Issue::ParamSpecial2)($context->getFile(), $context->getLineNumberStart(), [1, 'values', (string) $node_type, (string) $method->getFQSEN(), 'array']); })) { return; } } // The arginfo check will handle the other case break; default: break; } }
/** * Visit a node with kind `\ast\AST_METHOD` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitMethod(Decl $node) : Context { // Bomb out if we're not in a class context $clazz = $this->getContextClass(); $method_name = (string) $node->name; $method_fqsen = FullyQualifiedMethodName::fromStringInContext($method_name, $this->context); // Hunt for an available alternate ID if necessary $alternate_id = 0; while ($this->code_base->hasMethod($method_fqsen)) { $method_fqsen = $method_fqsen->withAlternateId(++$alternate_id); } // Create a new context with a new scope $context = $this->context->withScope(new Scope()); // Add $this to the scope of non-static methods if (!($node->flags & \ast\flags\MODIFIER_STATIC)) { assert($clazz->getContext()->getScope()->hasVariableWithName('this'), "Classes must have a \$this variable."); $context = $context->withScopeVariable($clazz->getContext()->getScope()->getVariableWithName('this')); } $method = Method::fromNode($context, $this->code_base, $node); // Override the FQSEN with the found alternate ID $method->setFQSEN($method_fqsen); $clazz->addMethod($this->code_base, $method); if ('__construct' === $method_name) { $clazz->setIsParentConstructorCalled(false); } else { if ('__invoke' === $method_name) { $clazz->getUnionType()->addType(CallableType::instance()); } else { if ('__toString' === $method_name && !$this->context->getIsStrictTypes()) { $clazz->getUnionType()->addType(StringType::instance()); } } } // Add each method parameter to the scope. We clone it // so that changes to the variable don't alter the // parameter definition foreach ($method->getParameterList() as $parameter) { $method->getContext()->addScopeVariable(clone $parameter); } // Send the context into the method and reset the scope $context = $method->getContext()->withMethodFQSEN($method->getFQSEN()); return $context; }