/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeParentConstructorCalled(CodeBase $code_base, Clazz $clazz) { // Only look at classes configured to require a call // to its parent constructor if (!in_array($clazz->getName(), Config::get()->parent_constructor_required)) { return; } // Don't worry about internal classes if ($clazz->isInternal()) { return; } // Don't worry if there's no parent class if (!$clazz->hasParentClassFQSEN()) { return; } if (!$code_base->hasClassWithFQSEN($clazz->getParentClassFQSEN())) { // This is an error, but its caught elsewhere. We'll // just roll through looking for other errors return; } $parent_clazz = $code_base->getClassByFQSEN($clazz->getParentClassFQSEN()); if (!$parent_clazz->isAbstract() && !$clazz->getIsParentConstructorCalled()) { Issue::emit(Issue::TypeParentConstructorCalled, $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart(), (string) $clazz->getFQSEN(), (string) $parent_clazz->getFQSEN()); } }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzePropertyTypes(CodeBase $code_base, Clazz $clazz) { foreach ($clazz->getPropertyList($code_base) as $property) { try { $union_type = $property->getUnionType(); } catch (IssueException $exception) { Issue::maybeEmitInstance($code_base, $property->getContext(), $exception->getIssueInstance()); continue; } // Look at each type in the parameter's Union Type foreach ($union_type->getTypeSet() as $type) { // If its a native type or a reference to // self, its OK if ($type->isNativeType() || $type->isSelfType()) { continue; } if ($type instanceof TemplateType) { if ($property->isStatic()) { Issue::maybeEmit($code_base, $property->getContext(), Issue::TemplateTypeStaticProperty, $property->getFileRef()->getLineNumberStart(), (string) $property->getFQSEN()); } } else { // Make sure the class exists $type_fqsen = $type->asFQSEN(); if (!$code_base->hasClassWithFQSEN($type_fqsen) && !$type instanceof TemplateType && (!$property->hasDefiningFQSEN() || $property->getDefiningFQSEN() == $property->getFQSEN())) { Issue::maybeEmit($code_base, $property->getContext(), Issue::UndeclaredTypeProperty, $property->getFileRef()->getLineNumberStart(), (string) $property->getFQSEN(), (string) $type_fqsen); } } } } }
/** * @return bool * True if the FQSEN exists. If not, a log line is emitted */ private static function fqsenExistsForClass(FQSEN $fqsen, CodeBase $code_base, Clazz $clazz, string $message_template) : bool { if (!$code_base->hasClassWithFQSEN($fqsen)) { Log::err(Log::EUNDEF, sprintf($message_template, $fqsen), $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart()); return false; } return true; }
/** * @return bool * True if the FQSEN exists. If not, a log line is emitted */ private static function fqsenExistsForClass(FQSEN $fqsen, CodeBase $code_base, Clazz $clazz) : bool { if (!$code_base->hasClassWithFQSEN($fqsen)) { Log::err(Log::EUNDEF, "Trying to inherit from unknown class {$fqsen}", $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart()); return false; } return true; }
/** * @return bool * True if the FQSEN exists. If not, a log line is emitted */ private static function fqsenExistsForClass(FQSEN $fqsen, CodeBase $code_base, Clazz $clazz, string $issue_type) : bool { if (!$code_base->hasClassWithFQSEN($fqsen)) { Issue::maybeEmit($code_base, $clazz->getContext(), $issue_type, $clazz->getFileRef()->getLineNumberStart(), (string) $fqsen); return false; } return true; }
/** * @param CodeBase $code_base * The code base in which the class exists * * @param Clazz $class * A class being analyzed * * @return void */ public function analyzeClass(CodeBase $code_base, Clazz $class) { // As an example, we test to see if the name of // the class is `Class`, and emit an issue explain that // the name is not allowed. if ($class->getName() == 'Class') { $this->emitIssue($code_base, $class->getContext(), 'DemoPluginClassName', "Class {$class->getFQSEN()} cannot be called `Class`"); } }
/** * @param array * A map from column name to value * * @return Model * An instance of the model derived from row data */ public static function fromRow(array $row) : Clazz { $parent_fqsen = $row['parent_class_fqsen'] ? FullyQualifiedClassName::fromFullyQualifiedString($row['parent_class_fqsen']) : null; $interface_fqsen_list = array_map(function (string $fqsen_string) { return FullyQualifiedClassName::fromFullyQualifiedString($fqsen_string); }, array_filter(explode('|', $row['interface_fqsen_list']))); $trait_fqsen_list = array_map(function (string $fqsen_string) { return FullyQualifiedClassName::fromFullyQualifiedString($fqsen_string); }, array_filter(explode('|', $row['trait_fqsen_list']))); $clazz = new ClazzElement(unserialize(base64_decode($row['context'])), $row['name'], UnionType::fromFullyQualifiedString($row['type']), (int) $row['flags'], $parent_fqsen, $interface_fqsen_list, $trait_fqsen_list); $clazz->setFQSEN(FullyQualifiedClassName::fromFullyQualifiedString($row['fqsen'])); return new Clazz($clazz); }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzePropertyTypes(CodeBase $code_base, Clazz $clazz) { foreach ($clazz->getPropertyList($code_base) as $property) { $union_type = $property->getUnionType(); // Look at each type in the parameter's Union Type foreach ($union_type->getTypeSet() 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)) { Issue::emit(Issue::UndeclaredTypeProperty, $property->getContext()->getFile(), $property->getContext()->getLineNumberStart(), (string) $type_fqsen); } } } }
/** * @return Method * A default constructor for the given class */ public static function defaultConstructorForClassInContext(Clazz $clazz, Context $context, CodeBase $code_base) : Method { $method_fqsen = FullyQualifiedMethodName::make($clazz->getFQSEN(), '__construct'); $method = new Method($context, '__construct', $clazz->getUnionType(), 0, $method_fqsen); if ($clazz->hasMethodWithName($code_base, $clazz->getName())) { $old_style_constructor = $clazz->getMethodByName($code_base, $clazz->getName()); $method->setParameterList($old_style_constructor->getParameterList()); $method->setNumberOfRequiredParameters($old_style_constructor->getNumberOfRequiredParameters()); $method->setNumberOfOptionalParameters($old_style_constructor->getNumberOfOptionalParameters()); } return $method; }
/** * @return Method * A default constructor for the given class */ public static function defaultConstructorForClassInContext(Clazz $clazz, Context $context) : Method { return new Method($context, '__construct', $clazz->getUnionType(), 0); }
/** * Add properties, constants and methods from the given * class to this. * * @param Clazz $superclazz * A class to import from * * @return null */ public function importAncestorClass(CodeBase $code_base, Clazz $superclazz) { $this->memoize((string) $superclazz->getFQSEN(), function () use($code_base, $superclazz) { // Copy properties foreach ($superclazz->getPropertyMap($code_base) as $property) { $this->addProperty($code_base, $property); } // Copy constants foreach ($superclazz->getConstantMap($code_base) as $constant) { $this->addConstant($code_base, $constant); } // Copy methods foreach ($superclazz->getMethodMap($code_base) as $method) { $this->addMethod($code_base, $method); } }); }
/** * Visit a node with kind `\ast\AST_CLASS` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitClass(Decl $node) : Context { if ($node->flags & \ast\flags\CLASS_ANONYMOUS) { $class_name = (new ContextNode($this->code_base, $this->context, $node))->getUnqualifiedNameForAnonymousClass(); } else { $class_name = (string) $node->name; } // This happens now and then and I have no idea // why. if (empty($class_name)) { return $this->context; } assert(!empty($class_name), "Class must have name in {$this->context}"); $class_fqsen = FullyQualifiedClassName::fromStringInContext($class_name, $this->context); assert($class_fqsen instanceof FullyQualifiedClassName, "The class FQSEN must be a FullyQualifiedClassName"); // Hunt for an available alternate ID if necessary $alternate_id = 0; while ($this->code_base->hasClassWithFQSEN($class_fqsen)) { $class_fqsen = $class_fqsen->withAlternateId(++$alternate_id); } // Build the class from what we know so far $class_context = $this->context->withLineNumberStart($node->lineno ?? 0)->withLineNumberEnd($node->endLineno ?? -1); $class = new Clazz($class_context, $class_name, $class_fqsen->asUnionType(), $node->flags ?? 0, $class_fqsen); // Set the scope of the class's context to be the // internal scope of the class $class_context = $class_context->withScope($class->getInternalScope()); // Get a comment on the class declaration $comment = Comment::fromStringInContext($node->docComment ?? '', $this->context); // Add any template types parameterizing a generic class foreach ($comment->getTemplateTypeList() as $template_type) { $class->getInternalScope()->addTemplateType($template_type); } $class->setIsDeprecated($comment->isDeprecated()); $class->setSuppressIssueList($comment->getSuppressIssueList()); // Add the class to the code base as a globally // accessible object $this->code_base->addClass($class); // Look to see if we have a parent class if (!empty($node->children['extends'])) { $parent_class_name = $node->children['extends']->children['name']; // Check to see if the name isn't fully qualified if ($node->children['extends']->flags & \ast\flags\NAME_NOT_FQ) { if ($this->context->hasNamespaceMapFor(T_CLASS, $parent_class_name)) { // Get a fully-qualified name $parent_class_name = (string) $this->context->getNamespaceMapFor(T_CLASS, $parent_class_name); } else { $parent_class_name = $this->context->getNamespace() . '\\' . $parent_class_name; } } // The name is fully qualified. Make sure it looks // like it is if (0 !== strpos($parent_class_name, '\\')) { $parent_class_name = '\\' . $parent_class_name; } $parent_fqsen = FullyQualifiedClassName::fromStringInContext($parent_class_name, $this->context); // Set the parent for the class $class->setParentType($parent_fqsen->asType()); } // If the class explicitly sets its overriding extension type, // set that on the class $inherited_type_option = $comment->getInheritedTypeOption(); if ($inherited_type_option->isDefined()) { $class->setParentType($inherited_type_option->get()); } // Add any implemeneted interfaces if (!empty($node->children['implements'])) { $interface_list = (new ContextNode($this->code_base, $this->context, $node->children['implements']))->getQualifiedNameList(); foreach ($interface_list as $name) { $class->addInterfaceClassFQSEN(FullyQualifiedClassName::fromFullyQualifiedString($name)); } } return $class_context; }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeDuplicateClass(CodeBase $code_base, Clazz $clazz) { // Determine if its a duplicate by looking to see if // the FQSEN is suffixed with an alternate ID. if (!$clazz->getFQSEN()->isAlternate()) { return; } $original_fqsen = $clazz->getFQSEN()->getCanonicalFQSEN(); if (!$code_base->hasClassWithFQSEN($original_fqsen)) { // If there's a missing class we'll catch that // elsewhere return; } // Get the original class $original_class = $code_base->getClassByFQSEN($original_fqsen); // Check to see if the original definition was from // an internal class if ($original_class->isInternal()) { Log::err(Log::EREDEF, "{$clazz} defined at " . "{$clazz->getContext()->getFile()}:{$clazz->getContext()->getLineNumberStart()} " . "was previously defined as {$original_class} internally", $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart()); // Otherwise, print the coordinates of the original // definition } else { Log::err(Log::EREDEF, "{$clazz} defined at " . "{$clazz->getContext()->getFile()}:{$clazz->getContext()->getLineNumberStart()} " . "was previously defined as {$original_class} at " . "{$original_class->getContext()->getFile()}:{$original_class->getContext()->getLineNumberStart()}", $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart()); } return; }
/** * Check to see if signatures match * * @return void */ public static function analyzeComposition(CodeBase $code_base, Clazz $class) { // Get the Class's FQSEN $fqsen = $class->getFQSEN(); // Get the list of all inherited classes. $inherited_class_list = $class->getInheritedClassList($code_base); // No chance of failed composition if we don't inherit from // lots of stuff. if (count($inherited_class_list) < 2) { return; } // For each property, find out every inherited class that defines it // and check to see if the types line up. foreach ($class->getPropertyList($code_base) as $property) { try { $property_union_type = $property->getUnionType(); } catch (IssueException $exception) { $property_union_type = new UnionType(); } // Check for that property on each inherited // class/trait/interface foreach ($inherited_class_list as $inherited_class) { // Skip any classes/traits/interfaces not defining that // property if (!$inherited_class->hasPropertyWithName($code_base, $property->getName())) { continue; } // We don't call `getProperty` because that will create // them in some circumstances. $inherited_property_map = $inherited_class->getPropertyMap($code_base); if (!isset($inherited_property_map[$property->getName()])) { continue; } // Get the inherited property $inherited_property = $inherited_property_map[$property->getName()]; // Figure out if this property type can cast to the // inherited definition's type. $can_cast = $property_union_type->canCastToExpandedUnionType($inherited_property->getUnionType(), $code_base); if ($can_cast) { continue; } // Don't emit an issue if the property suppresses the issue if ($property->hasSuppressIssue(Issue::IncompatibleCompositionProp)) { continue; } Issue::maybeEmit($code_base, $property->getContext(), Issue::IncompatibleCompositionProp, $property->getFileRef()->getLineNumberStart(), (string) $class->getFQSEN(), (string) $inherited_class->getFQSEN(), $property->getName(), (string) $class->getFQSEN(), $class->getFileRef()->getFile(), $class->getFileRef()->getLineNumberStart()); } } // TODO: This has too much overlap with PhanParamSignatureMismatch // and we should figure out how to merge it. /* $method_map = $code_base->getMethodMapByFullyQualifiedClassName($fqsen); // For each method, find out every inherited class that defines it // and check to see if the types line up. foreach ($method_map as $i => $method) { $method_union_type = $method->getUnionType(); // We don't need to analyze constructors for signature // compatibility if ($method->getName() == '__construct') { continue; } // Get the method parameter list // Check for that method on each inherited // class/trait/interface foreach ($inherited_class_list as $inherited_class) { // Skip anything that doesn't define this method if (!$inherited_class->hasMethodWithName($code_base, $method->getName())) { continue; } $inherited_method = $inherited_class->getMethodByName($code_base, $method->getName()); if ($method == $inherited_method) { continue; } // Figure out if this method return type can cast to the // inherited definition's return type. $is_compatible = $method_union_type->canCastToExpandedUnionType( $inherited_method->getUnionType(), $code_base ); $inherited_method_parameter_map = $inherited_method->getParameterList(); // Figure out if all of the parameter types line up foreach ($method->getParameterList() as $i => $parameter) { $is_compatible = ( $is_compatible && isset($inherited_method_parameter_map[$i]) && $parameter->getUnionType()->canCastToExpandedUnionType( ($inherited_method_parameter_map[$i])->getUnionType(), $code_base ) ); } if ($is_compatible) { continue; } // Don't emit an issue if the method suppresses the issue if ($method->hasSuppressIssue(Issue::IncompatibleCompositionMethod)) { continue; } Issue::maybeEmit( $code_base, $method->getContext(), Issue::IncompatibleCompositionMethod, $method->getFileRef()->getLineNumberStart(), (string)$method, (string)$inherited_method, $inherited_method->getFileRef()->getFile(), $inherited_method->getFileRef()->getLineNumberStart() ); } } */ }
/** * @param Clazz $class * A class to add. * * @return void */ public function addClass(Clazz $class) { // Map the FQSEN to the class $this->fqsen_class_map[$class->getFQSEN()] = $class; }
/** * Add properties, constants and methods from the given * class to this. * * @param CodeBase $code_base * A reference to the code base in which the ancestor exists * * @param Clazz $class * A class to import from * * @param Option<Type>|None $type_option * A possibly defined ancestor type used to define template * parameter types when importing ancestor properties and * methods * * @return void */ public function importAncestorClass(CodeBase $code_base, Clazz $class, $type_option) { if (!$this->isFirstExecution(__METHOD__ . ':' . (string) $class->getFQSEN())) { return; } $class->addReference($this->getContext()); // Make sure that the class imports its parents first $class->hydrate($code_base); // Copy properties foreach ($class->getPropertyMap($code_base) as $property) { $this->addProperty($code_base, $property, $type_option); } // Copy constants foreach ($class->getConstantMap($code_base) as $constant) { $this->addConstant($code_base, $constant); } // Copy methods foreach ($class->getMethodMap($code_base) as $method) { $this->addMethod($code_base, $method, $type_option); } }
/** * @return array * Get a map from column name to row values for * this instance */ public function toRow() : array { $parent_class_fqsen = $this->clazz->hasParentClassFQSEN() ? (string) $this->clazz->getParentClassFQSEN() : null; $interface_fqsen_list_string = implode('|', array_map(function (FullyQualifiedClassName $fqsen) { return (string) $fqsen; }, $this->clazz->getInterfaceFQSENList())); $trait_fqsen_list_string = implode('|', array_map(function (FullyQualifiedClassName $fqsen) { return (string) $fqsen; }, $this->clazz->getInterfaceFQSENList())); return ['name' => (string) $this->clazz->getName(), 'type' => (string) $this->clazz->getUnionType(), 'flags' => $this->clazz->getFlags(), 'fqsen' => (string) $this->clazz->getFQSEN(), 'context' => base64_encode(serialize($this->clazz->getContext())), 'is_deprecated' => $this->clazz->isDeprecated(), 'parent_class_fqsen' => $parent_class_fqsen, 'interface_fqsen_list' => $interface_fqsen_list_string, 'trait_fqsen_list' => $trait_fqsen_list_string, 'is_parent_constructor_called' => $this->clazz->getIsParentConstructorCalled()]; }
/** * Visit a node with kind `\ast\AST_CLASS` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitClass(Decl $node) : Context { if ($node->flags & \ast\flags\CLASS_ANONYMOUS) { $class_name = (new ContextNode($this->code_base, $this->context, $node))->getUnqualifiedNameForAnonymousClass(); } else { $class_name = (string) $node->name; } // This happens now and then and I have no idea // why. if (empty($class_name)) { return $this->context; } assert(!empty($class_name), "Class must have name in {$this->context}"); $class_fqsen = FullyQualifiedClassName::fromStringInContext($class_name, $this->context); // Hunt for an available alternate ID if necessary $alternate_id = 0; while ($this->code_base->hasClassWithFQSEN($class_fqsen)) { $class_fqsen = $class_fqsen->withAlternateId(++$alternate_id); } // Build the class from what we know so far $class_context = $this->context->withLineNumberStart($node->lineno ?? 0)->withLineNumberEnd($node->endLineno ?? -1); $clazz = new Clazz($class_context, $class_name, UnionType::fromStringInContext($class_name, $this->context), $node->flags ?? 0); // Override the FQSEN with the found alternate ID $clazz->setFQSEN($class_fqsen); // Get a comment on the class declaration $comment = Comment::fromStringInContext($node->docComment ?? '', $this->context); $clazz->setIsDeprecated($comment->isDeprecated()); $clazz->setSuppressIssueList($comment->getSuppressIssueList()); // Add the class to the code base as a globally // accessible object $this->code_base->addClass($clazz); // Look to see if we have a parent class if (!empty($node->children['extends'])) { $parent_class_name = $node->children['extends']->children['name']; // Check to see if the name isn't fully qualified if ($node->children['extends']->flags & \ast\flags\NAME_NOT_FQ) { if ($this->context->hasNamespaceMapFor(T_CLASS, $parent_class_name)) { // Get a fully-qualified name $parent_class_name = (string) $this->context->getNamespaceMapFor(T_CLASS, $parent_class_name); } else { $parent_class_name = $this->context->getNamespace() . '\\' . $parent_class_name; } } // The name is fully qualified. Make sure it looks // like it is if (0 !== strpos($parent_class_name, '\\')) { $parent_class_name = '\\' . $parent_class_name; } $parent_fqsen = FullyQualifiedClassName::fromStringInContext($parent_class_name, $this->context); // Set the parent for the class $clazz->setParentClassFQSEN($parent_fqsen); } // Add any implemeneted interfaces if (!empty($node->children['implements'])) { $interface_list = (new ContextNode($this->code_base, $this->context, $node->children['implements']))->getQualifiedNameList(); foreach ($interface_list as $name) { $clazz->addInterfaceClassFQSEN(FullyQualifiedClassName::fromFullyQualifiedString($name)); } } // Update the context to signal that we're now // within a class context. $context = $class_context->withClassFQSEN($class_fqsen); return $context; }
/** * @return Method * A default constructor for the given class */ public static function defaultConstructorForClassInContext(Clazz $clazz, Context $context) : Method { $method = new Method($context, '__construct', $clazz->getUnionType(), 0); $method->setFQSEN(FullyQualifiedMethodName::make($clazz->getFQSEN(), '__construct')); return $method; }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeDuplicateClass(CodeBase $code_base, Clazz $clazz) { // Determine if its a duplicate by looking to see if // the FQSEN is suffixed with an alternate ID. if (!$clazz->getFQSEN()->isAlternate()) { return; } $original_fqsen = $clazz->getFQSEN()->getCanonicalFQSEN(); if (!$code_base->hasClassWithFQSEN($original_fqsen)) { // If there's a missing class we'll catch that // elsewhere return; } // Get the original class $original_class = $code_base->getClassByFQSEN($original_fqsen); // Check to see if the original definition was from // an internal class if ($original_class->isInternal()) { Issue::emit(Issue::RedefineClassInternal, $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart(), (string) $clazz, $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart(), (string) $original_class); // Otherwise, print the coordinates of the original // definition } else { Issue::emit(Issue::RedefineClass, $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart(), (string) $clazz, $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart(), (string) $original_class, $original_class->getContext()->getFile(), $original_class->getContext()->getLineNumberStart()); } return; }
/** * @param string[] $class_name_list * A list of class names to load type information for * * @return null */ private function addClassesByNames(array $class_name_list) { foreach ($class_name_list as $i => $class_name) { $clazz = Clazz::fromClassName($this, $class_name); $this->class_map[$clazz->getFQSEN()] = $clazz; } }