/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeParameterTypes(CodeBase $code_base, Method $method) { // Look at each method parameter foreach ($method->getParameterList() as $parameter) { $union_type = $parameter->getUnionType(); // Look at each type in the parameter's Union Type foreach ($union_type->getTypeList() as $type) { // If its a native type or a reference to // self, its OK if ($type->isNativeType() || $type->isSelfType()) { continue; } // Otherwise, make sure the class exists $type_fqsen = $type->asFQSEN(); if (!$code_base->hasClassWithFQSEN($type_fqsen)) { Log::err(Log::EUNDEF, "parameter of undeclared type {$type_fqsen}", $method->getContext()->getFile(), $method->getContext()->getLineNumberStart()); } } } }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeDuplicateFunction(CodeBase $code_base, Method $method) { $fqsen = $method->getFQSEN(); if (!$fqsen->isAlternate()) { return; } $original_fqsen = $fqsen->getCanonicalFQSEN(); if (!$code_base->hasMethod($original_fqsen)) { return; } $original_method = $code_base->getMethod($original_fqsen); $method_name = $method->getName(); if ('internal' === $original_method->getContext()->getFile()) { Issue::emit(Issue::RedefineFunctionInternal, $method->getContext()->getFile(), $method->getContext()->getLineNumberStart(), $method_name, $method->getContext()->getFile(), $method->getContext()->getLineNumberStart()); } else { Issue::emit(Issue::RedefineFunction, $method->getContext()->getFile(), $method->getContext()->getLineNumberStart(), $method_name, $method->getContext()->getFile(), $method->getContext()->getLineNumberStart(), $original_method->getContext()->getFile(), $original_method->getContext()->getLineNumberStart()); } }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeDuplicateFunction(CodeBase $code_base, Method $method) { $fqsen = $method->getFQSEN(); if (!$fqsen->isAlternate()) { return; } $original_fqsen = $fqsen->getCanonicalFQSEN(); if (!$code_base->hasMethod($original_fqsen)) { return; } $original_method = $code_base->getMethod($original_fqsen); $method_name = $method->getName(); if ('internal' === $original_method->getContext()->getFile()) { // If its in an conditional and the original is an // internal method, presume its all OK. if ($method->getContext()->getIsConditional()) { return; } Log::err(Log::EREDEF, "Function {$method_name} defined at {$method->getContext()->getFile()}:{$method->getContext()->getLineNumberStart()} was previously defined internally", $method->getContext()->getFile(), $method->getContext()->getLineNumberStart()); } else { Log::err(Log::EREDEF, "Function {$method_name} defined at {$method->getContext()->getFile()}:{$method->getContext()->getLineNumberStart()} was previously defined at {$original_method->getContext()->getFile()}:{$original_method->getContext()->getLineNumberStart()}", $method->getContext()->getFile(), $method->getContext()->getLineNumberStart()); } }
/** * @param Method $method * Get a list of methods hydrated with type information * for the given partial method * * @param CodeBase $code_base * The global code base holding all state * * @return Method[] * A list of typed methods based on the given method */ private static function methodListFromMethod(Method $method, CodeBase $code_base) : array { // See if we have any type information for this // internal function $map_list = UnionType::internalFunctionSignatureMapForFQSEN($method->getFQSEN()); if (!$map_list) { return [$method]; } $alternate_id = 0; return array_map(function ($map) use($method, &$alternate_id) : Method { $alternate_method = clone $method; $alternate_method->setFQSEN($alternate_method->getFQSEN()->withAlternateId($alternate_id++)); // Set the return type if one is defined if (!empty($map['return_type'])) { $alternate_method->setUnionType($map['return_type']); } // Load properties if defined foreach ($map['property_name_type_map'] ?? [] as $parameter_name => $parameter_type) { $flags = 0; $is_optional = false; // Check to see if its a pass-by-reference parameter if (strpos($parameter_name, '&') === 0) { $flags |= \ast\flags\PARAM_REF; $parameter_name = substr($parameter_name, 1); } // Check to see if its variadic if (strpos($parameter_name, '...') !== false) { $flags |= \ast\flags\PARAM_VARIADIC; $parameter_name = str_replace('...', '', $parameter_name); } // Check to see if its an optional parameter if (strpos($parameter_name, '=') !== false) { $is_optional = true; $parameter_name = str_replace('=', '', $parameter_name); } $parameter = new Parameter($method->getContext(), $parameter_name, $parameter_type, $flags); if ($is_optional) { $parameter->setDefaultValue(null, NullType::instance()->asUnionType()); } // Add the parameter $alternate_method->parameter_list[] = $parameter; } return $alternate_method; }, $map_list); }
/** * Make sure signatures line up between methods and the * methods they override * * @see https://en.wikipedia.org/wiki/Liskov_substitution_principle */ private static function analyzeOverrideSignature(CodeBase $code_base, Method $method) { if (!Config::get()->analyze_signature_compatibility) { return; } // Hydrate the class this method is coming from in // order to understand if its an override or not $class = $method->getClass($code_base); $class->hydrate($code_base); // Check to see if the method is an override // $method->analyzeOverride($code_base); // Make sure we're actually overriding something if (!$method->getIsOverride()) { return; } // Dont' worry about signatures lining up on // constructors. We just want to make sure that // calling a method on a subclass won't cause // a runtime error. We usually know what we're // constructing at instantiation time, so there // is less of a risk. if ($method->getName() == '__construct') { return; } // Get the method that is being overridden $o_method = $method->getOverriddenMethod($code_base); // Get the class that the overridden method lives on $o_class = $o_method->getClass($code_base); // PHP doesn't complain about signature mismatches // with traits, so neither shall we if ($o_class->isTrait()) { return; } // Get the parameters for that method $o_parameter_list = $o_method->getParameterList(); // If we have a parent type defined, map the method's // return type and parameter types through it $type_option = $class->getParentTypeOption(); // Map overridden method parameter types through any // template type parameters we may have if ($type_option->isDefined()) { $o_parameter_list = array_map(function (Parameter $parameter) use($type_option, $code_base) : Parameter { if (!$parameter->getUnionType()->hasTemplateType()) { return $parameter; } $mapped_parameter = clone $parameter; $mapped_parameter->setUnionType($mapped_parameter->getUnionType()->withTemplateParameterTypeMap($type_option->get()->getTemplateParameterTypeMap($code_base))); return $mapped_parameter; }, $o_parameter_list); } // Map overridden method return type through any template // type parameters we may have $o_return_union_type = $o_method->getUnionType(); if ($type_option->isDefined() && $o_return_union_type->hasTemplateType()) { $o_return_union_type = $o_return_union_type->withTemplateParameterTypeMap($type_option->get()->getTemplateParameterTypeMap($code_base)); } // Determine if the signatures match up $signatures_match = true; // Make sure the count of parameters matches if ($method->getNumberOfRequiredParameters() > $o_method->getNumberOfRequiredParameters()) { $signatures_match = false; } else { if ($method->getNumberOfParameters() < $o_method->getNumberOfParameters()) { $signatures_match = false; // If parameter counts match, check their types } else { foreach ($method->getParameterList() as $i => $parameter) { if (!isset($o_parameter_list[$i])) { continue; } $o_parameter = $o_parameter_list[$i]; // Changing pass by reference is not ok // @see https://3v4l.org/Utuo8 if ($parameter->isPassByReference() != $o_parameter->isPassByReference()) { $signatures_match = false; break; } // A stricter type on an overriding method is cool if ($o_parameter->getUnionType()->isEmpty() || $o_parameter->getUnionType()->isType(MixedType::instance())) { continue; } // Its not OK to have a more relaxed type on an // overriding method // // https://3v4l.org/XTm3P if ($parameter->getUnionType()->isEmpty()) { $signatures_match = false; break; } // If we have types, make sure they line up // // TODO: should we be expanding the types on $o_parameter // via ->asExpandedTypes($code_base)? // // @see https://3v4l.org/ke3kp if (!$o_parameter->getUnionType()->canCastToUnionType($parameter->getUnionType())) { $signatures_match = false; break; } } } } // Return types should be mappable if (!$o_return_union_type->isEmpty()) { if (!$method->getUnionType()->asExpandedTypes($code_base)->canCastToUnionType($o_return_union_type)) { $signatures_match = false; } } // Static or non-static should match if ($method->isStatic() != $o_method->isStatic()) { if ($o_method->isStatic()) { Issue::maybeEmit($code_base, $method->getContext(), Issue::AccessStaticToNonStatic, $method->getFileRef()->getLineNumberStart(), $o_method->getFQSEN()); } else { Issue::maybeEmit($code_base, $method->getContext(), Issue::AccessNonStaticToStatic, $method->getFileRef()->getLineNumberStart(), $o_method->getFQSEN()); } } if ($o_method->returnsRef() && !$method->returnsRef()) { $signatures_match = false; } if (!$signatures_match) { if ($o_method->isInternal()) { Issue::maybeEmit($code_base, $method->getContext(), Issue::ParamSignatureMismatchInternal, $method->getFileRef()->getLineNumberStart(), $method, $o_method); } else { Issue::maybeEmit($code_base, $method->getContext(), Issue::ParamSignatureMismatch, $method->getFileRef()->getLineNumberStart(), $method, $o_method, $o_method->getFileRef()->getFile(), $o_method->getFileRef()->getLineNumberStart()); } } // Access must be compatible if ($o_method->isProtected() && $method->isPrivate() || $o_method->isPublic() && !$method->isPublic()) { if ($o_method->isInternal()) { Issue::maybeEmit($code_base, $method->getContext(), Issue::AccessSignatureMismatchInternal, $method->getFileRef()->getLineNumberStart(), $method, $o_method); } else { Issue::maybeEmit($code_base, $method->getContext(), Issue::AccessSignatureMismatch, $method->getFileRef()->getLineNumberStart(), $method, $o_method, $o_method->getFileRef()->getFile(), $o_method->getFileRef()->getLineNumberStart()); } } }
/** * @param CodeBase $code_base * The global code base * * @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 * * @return null * * @see \Phan\Deprecated\Pass2::arglist_type_check * Formerly `function arglist_type_check` */ private static function analyzeParameterList(CodeBase $code_base, Method $method, Node $node, Context $context) { foreach ($node->children ?? [] as $i => $argument) { // Get the parameter associated with this argument $parameter = $method->getParameterList()[$i] ?? null; // This issue should be caught elsewhere if (!$parameter) { continue; } // If this is a pass-by-reference parameter, make sure // we're passing an allowable argument if ($parameter->isPassByReference()) { if (!$argument instanceof \ast\Node || $argument->kind != \ast\AST_VAR && $argument->kind != \ast\AST_DIM && $argument->kind != \ast\AST_PROP && $argument->kind != \ast\AST_STATIC_PROP) { Log::err(Log::ETYPE, "Only variables can be passed by reference at arg#" . ($i + 1) . " of {$method->getFQSEN()}()", $context->getFile(), $node->lineno); } else { $variable_name = AST::variableName($argument); if ($argument->kind == \ast\AST_STATIC_PROP) { if (in_array($variable_name, ['self', 'static', 'parent'])) { Log::err(Log::ESTATIC, "Using {$variable_name}:: when not in object context", $context->getFile(), $argument->lineno); } } } } // Get the type of the argument. We'll check it against // the parameter in a moment $argument_type = UnionType::fromNode($context, $code_base, $argument); // Expand it to include all parent types up the chain $argument_type_expanded = $argument_type->asExpandedTypes($code_base); /* TODO see issue #42 If argument is an object and it has a String union type, then we need to ignore that in strict_types=1 mode. if ($argument instanceof \ast\Node) { if(!empty($argument->children['class'])) { // arg is an object if ($method->getContext()->getStrictTypes()) { ... } } } or maybe UnionType::fromNode should check strict_types and not return the string union type or we shouldn't add the string type at all when a class has a __toString() and instead set a flag and check that instead */ // Check the method to see if it has the correct // parameter types. If not, keep hunting through // alternates of the method until we find one that // takes the correct types $alternate_parameter = null; $alternate_found = false; foreach ($method->alternateGenerator($code_base) as $alternate_id => $alternate_method) { if (empty($alternate_method->getParameterList()[$i])) { continue; } // Get the parameter associated with this argument $alternate_parameter = $alternate_method->getParameterList()[$i] ?? null; // Expand the types to find all parents and traits $alternate_parameter_type_expanded = $alternate_parameter->getUnionType()->asExpandedTypes($code_base); // See if the argument can be cast to the // parameter if ($argument_type_expanded->canCastToUnionType($alternate_parameter_type_expanded)) { $alternate_found = true; break; } } if (!$alternate_found) { $parameter_name = $alternate_parameter ? $alternate_parameter->getName() : 'unknown'; $parameter_type = $alternate_parameter ? $alternate_parameter->getUnionType() : 'unknown'; if ($method->getContext()->isInternal()) { Log::err(Log::ETYPE, "arg#" . ($i + 1) . "({$parameter_name}) is " . "{$argument_type_expanded} but {$method->getFQSEN()}() " . "takes {$parameter_type}", $context->getFile(), $node->lineno); } else { Log::err(Log::ETYPE, "arg#" . ($i + 1) . "({$parameter_name}) is " . "{$argument_type_expanded} but {$method->getFQSEN()}() " . "takes {$parameter_type} " . "defined at {$method->getContext()->getFile()}:{$method->getContext()->getLineNumberStart()}", $context->getFile(), $node->lineno); } } } }
/** * @return array * Get a map from column name to row values for * this instance */ public function toRow() : array { return ['scope_name' => $this->primaryKeyValue(), 'fqsen' => (string) $this->method->getFQSEN(), 'name' => (string) $this->method->getName(), 'type' => (string) $this->method->getUnionType(), 'flags' => $this->method->getFlags(), 'context' => base64_encode(serialize($this->method->getContext())), 'is_deprecated' => $this->method->isDeprecated(), 'number_of_required_parameters' => $this->method->getNumberOfRequiredParameters(), 'number_of_optional_parameters' => $this->method->getNumberOfOptionalParameters(), 'is_dynamic' => $this->method->isDynamic()]; }
/** * @param CodeBase $code_base * The global code base * * @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 * * @return null * * @see \Phan\Deprecated\Pass2::arglist_type_check * Formerly `function arglist_type_check` */ private static function analyzeParameterList(CodeBase $code_base, Method $method, Node $node, Context $context) { foreach ($node->children ?? [] as $i => $argument) { // Get the parameter associated with this argument $parameter = $method->getParameterList()[$i] ?? null; // This issue should be caught elsewhere if (!$parameter) { continue; } // If this is a pass-by-reference parameter, make sure // we're passing an allowable argument if ($parameter->isPassByReference()) { if (!$argument instanceof \ast\Node || $argument->kind != \ast\AST_VAR && $argument->kind != \ast\AST_DIM && $argument->kind != \ast\AST_PROP && $argument->kind != \ast\AST_STATIC_PROP) { Issue::emit(Issue::TypeNonVarPassByRef, $context->getFile(), $node->lineno ?? 0, $i + 1, (string) $method->getFQSEN()); } else { $variable_name = (new ContextNode($code_base, $context, $argument))->getVariableName(); if ($argument->kind == \ast\AST_STATIC_PROP) { if (in_array($variable_name, ['self', 'static', 'parent'])) { Issue::emit(Issue::ContextNotObject, $context->getFile(), $node->lineno ?? 0, "\${$variable_name}"); } } } } // Get the type of the argument. We'll check it against // the parameter in a moment $argument_type = UnionType::fromNode($context, $code_base, $argument); // Expand it to include all parent types up the chain $argument_type_expanded = $argument_type->asExpandedTypes($code_base); // Check the method to see if it has the correct // parameter types. If not, keep hunting through // alternates of the method until we find one that // takes the correct types $alternate_parameter = null; $alternate_found = false; foreach ($method->alternateGenerator($code_base) as $alternate_id => $alternate_method) { if (empty($alternate_method->getParameterList()[$i])) { continue; } // Get the parameter associated with this argument $alternate_parameter = $alternate_method->getParameterList()[$i] ?? null; // Expand the types to find all parents and traits $alternate_parameter_type_expanded = $alternate_parameter->getUnionType()->asExpandedTypes($code_base); // See if the argument can be cast to the // parameter if ($argument_type_expanded->canCastToUnionType($alternate_parameter_type_expanded)) { $alternate_found = true; break; } } if (!$alternate_found) { $parameter_name = $alternate_parameter ? $alternate_parameter->getName() : 'unknown'; $parameter_type = $alternate_parameter ? $alternate_parameter->getUnionType() : 'unknown'; if ($method->getContext()->isInternal()) { Issue::emit(Issue::TypeMismatchArgumentInternal, $context->getFile(), $node->lineno ?? 0, $i + 1, $parameter_name, $argument_type_expanded, (string) $method->getFQSEN(), (string) $parameter_type); } else { Issue::emit(Issue::TypeMismatchArgument, $context->getFile(), $node->lineno ?? 0, $i + 1, $parameter_name, $argument_type_expanded, (string) $method->getFQSEN(), (string) $parameter_type, $method->getContext()->getFile(), $method->getContext()->getLineNumberStart()); } } } }
private function addMethodWithScopeAndName(Method $method, string $scope, string $name) { $this->method_map[$scope][$name] = $method; // For elements that aren't internal PHP classes // if (!$method->getContext()->isInternal()) { // Associate the element with the file it was found in $this->getFileByPath($method->getContext()->getFile())->addMethodFQSEN($method->getFQSEN()); // } }
/** * @param CodeBase $code_base * The code base in which the method exists * * @param Method $method * A method being analyzed * * @return void */ public function analyzeMethod(CodeBase $code_base, Method $method) { // As an example, we test to see if the name of the // method is `function`, and emit an issue if it is. if ($method->getName() == 'function') { $this->emitIssue($code_base, $method->getContext(), 'DemoPluginMethodName', "Method {$method->getFQSEN()} cannot be called `function`"); } }
private function addMethodWithScopeAndName(Method $method, string $scope, string $name) { $this->method_map[$scope][$name] = $method; // If we're doing dead code detection, map the name // directly to the method so we can quickly look up // all methods with that name to add a possible // reference if (Config::get()->dead_code_detection) { $this->method_name_map[strtolower($name)][] = $method; } // Associate the element with the file it was found in $this->getFileByPath($method->getContext()->getFile())->addMethodFQSEN($method->getFQSEN()); }