/** * @param Context $context * The context of the current execution * * @param CodeBase $code_base * The global code base * * @param string $class_name * The name we're trying to validate */ public function __construct(Context $context, CodeBase $code_base, string $class_name) { $this->context = $context; $this->code_base = $code_base; $this->class_name = $class_name; // Compute the FQSEN based on the current context $this->class_fqsen = FullyQualifiedClassName::fromStringInContext($this->class_name, $this->context); }
public function testFullyQualifiedClassName() { $this->assertFQSENEqual(FullyQualifiedClassName::make('Name\\Space', 'A'), '\\Name\\Space\\a'); $this->assertFQSENEqual(FullyQualifiedClassName::make('', 'A'), '\\a'); $this->assertFQSENEqual(FullyQualifiedClassName::fromFullyQualifiedString('A'), '\\a'); $this->assertFQSENEqual(FullyQualifiedClassName::fromFullyQualifiedString('\\Name\\Space\\A'), '\\Name\\Space\\a'); $this->assertFQSENEqual(FullyQualifiedClassName::fromFullyQualifiedString('\\Namespace\\A,1'), '\\Namespace\\a,1'); $this->assertFQSENEqual(FullyQualifiedClassName::fromStringInContext('\\Namespace\\A', $this->context), '\\Namespace\\a'); $this->assertFQSENEqual(FullyQualifiedClassName::fromStringInContext('A', $this->context), '\\a'); }
/** * 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; } assert(!empty($class_name), "Class name cannot be empty"); $alternate_id = 0; // Hunt for the alternate of this class defined // in this file do { $class_fqsen = FullyQualifiedClassName::fromStringInContext($class_name, $this->context)->withAlternateId($alternate_id++); if (!$this->code_base->hasClassWithFQSEN($class_fqsen)) { throw new CodeBaseException($class_fqsen, "Can't find class {$class_fqsen} - aborting"); } $clazz = $this->code_base->getClassByFQSEN($class_fqsen); } while ($this->context->getProjectRelativePath() != $clazz->getContext()->getProjectRelativePath() || $this->context->getLineNumberStart() != $clazz->getContext()->getLineNumberStart()); return $clazz->getContext()->withClassFQSEN($class_fqsen); }
/** * Visit a node with kind `\ast\AST_USE_TRAIT` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitUseTrait(Node $node) : Context { // Bomb out if we're not in a class context $clazz = $this->getContextClass(); $trait_fqsen_string_list = (new ContextNode($this->code_base, $this->context, $node->children['traits']))->getQualifiedNameList(); // Add each trait to the class foreach ($trait_fqsen_string_list as $trait_fqsen_string) { $trait_fqsen = FullyQualifiedClassName::fromStringInContext($trait_fqsen_string, $this->context); $clazz->addTraitFQSEN($trait_fqsen); } return $this->context; }
/** * @return FQSEN */ public function getFQSEN() : FullyQualifiedClassName { // Allow overrides if ($this->fqsen) { return $this->fqsen; } return FullyQualifiedClassName::fromStringInContext($this->getName(), $this->getContext()); }
private function visitClassNode(Node $node) : UnionType { // Things of the form `new $class_name();` if ($node->kind == \ast\AST_VAR) { return new UnionType(); } // Anonymous class of form `new class { ... }` if ($node->kind == \ast\AST_CLASS && $node->flags & \ast\flags\CLASS_ANONYMOUS) { // Generate a stable name for the anonymous class $anonymous_class_name = (new ContextNode($this->code_base, $this->context, $node))->getUnqualifiedNameForAnonymousClass(); // Turn that into a fully qualified name $fqsen = FullyQualifiedClassName::fromStringInContext($anonymous_class_name, $this->context); // Turn that into a union type return Type::fromFullyQualifiedString((string) $fqsen)->asUnionType(); } // Things of the form `new $method->name()` if ($node->kind !== \ast\AST_NAME) { return new UnionType(); } // Get the name of the class $class_name = $node->children['name']; // If this is a straight-forward class name, recurse into the // class node and get its type if (!Type::isSelfTypeString($class_name)) { // TODO: does anyone else call this method? return self::unionTypeFromClassNode($this->code_base, $this->context, $node); } // This is a self-referential node if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, "Cannot access {$class_name} when not in a class scope", $this->context->getFile(), $node->lineno); return new UnionType(); } // Reference to a parent class if ($class_name === 'parent') { $class = $this->context->getClassInScope($this->code_base); if (!$class->hasParentClassFQSEN()) { Log::err(Log::ESTATIC, "Reference to parent of parentless class {$class->getFQSEN()}", $this->context->getFile(), $node->lineno); return new UnionType(); } return Type::fromFullyQualifiedString((string) $class->getParentClassFQSEN())->asUnionType(); } return Type::fromFullyQualifiedString((string) $this->context->getClassFQSEN())->asUnionType(); }
/** * @param Context $context * The context in which the FQSEN string was found * * @param $fqsen_string * An FQSEN string like '\Namespace\Class::methodName' * * @return FullyQualifiedMethodName */ public static function fromStringInContext(string $fqsen_string, Context $context) { // Test to see if we have a class defined if (false === strpos($fqsen_string, '::')) { assert($context->isInClassScope(), "Cannot reference class element without class name when not in class scope."); $fully_qualified_class_name = $context->getClassFQSEN(); } else { assert(false !== strpos($fqsen_string, '::'), "Fully qualified class element lacks '::' delimiter"); list($class_name_string, $fqsen_string) = explode('::', $fqsen_string); $fully_qualified_class_name = FullyQualifiedClassName::fromStringInContext($class_name_string, $context); } // Split off the alternate ID $parts = explode(',', $fqsen_string); $name = $parts[0]; $alternate_id = (int) ($parts[1] ?? 0); assert(is_int($alternate_id), "Alternate must be an integer"); return static::make($fully_qualified_class_name, $name, $alternate_id); }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitCatch(Node $node) : Context { // Get the name of the class $class_name = $node->children['class']->children['name']; $clazz = null; // If we can't figure out the class name (which happens // from time to time), then give up if (!empty($class_name)) { $class_fqsen = FullyQualifiedClassName::fromStringInContext($class_name, $this->context); // Check to see if the class actually exists if ($this->code_base->hasClassWithFQSEN($class_fqsen)) { $clazz = $this->code_base->getClassByFQSEN($class_fqsen); } else { Log::err(Log::EUNDEF, "call to method on undeclared class {$class_name}", $this->context->getFile(), $node->lineno); } } $variable_name = AST::variableName($node->children['var']); if (!empty($variable_name)) { $variable = Variable::fromNodeInContext($node->children['var'], $this->context, $this->code_base, false); if ($clazz) { $variable->setUnionType($clazz->getUnionType()); } $this->context->addScopeVariable($variable); } return $this->context; }
/** * @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 string * The class name represented by the given call */ public function visitVar(Node $node) : string { // $$var->method() if ($node->children['name'] instanceof Node) { return ''; } // $this->method() if ($node->children['name'] == 'this') { if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, 'Using $this when not in object context', $this->context->getFile(), $node->lineno); return ''; } return (string) $this->context->getClassFQSEN(); } $variable_name = $node->children['name']; if (!$this->context->getScope()->hasVariableWithName($variable_name)) { // Got lost, couldn't find the variable in the current scope // If it really isn't defined, it will be caught by the // undefined var error return ''; } $variable = $this->context->getScope()->getVariableWithName($variable_name); // Hack - loop through the possible types of the var and assume // first found class is correct foreach ($variable->getUnionType()->nonGenericArrayTypes()->getTypeList() as $type) { $child_class_fqsen = FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); if ($this->code_base->hasClassWithFQSEN($child_class_fqsen)) { return (string) FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); } } // We land here if we have a variable // with a native type or no known type. return ''; }
/** * @param Node $node * The node that has a reference to a class * * @param Context $context * The context in which we found the node * * @param CodeBase $code_base * The global code base holding all state * * @param bool $validate_class_name * If true, we'll validate that the name of the class * is valid. * * @return Clazz * The class being referenced in the given node in * the given context * * @throws NodeException * An exception is thrown if we can't understand the node * * @throws CodeBaseExtension * An exception is thrown if we can't find the referenced * class * * @throws TypeException * An exception may be thrown if the only viable candidate * is a non-class type. */ public static function classFromNodeInContext(Node $node, Context $context, CodeBase $code_base, bool $validate_class_name = true) : Clazz { // Figure out the name of the class $class_name = self::classNameFromNode($context, $code_base, $node, $validate_class_name); // If we can't figure out the class name (which happens // from time to time), then give up if (empty($class_name)) { throw new NodeException($node, 'Could not find class name'); } $class_fqsen = FullyQualifiedClassName::fromStringInContext($class_name, $context); // Check to see if the class actually exists if (!$code_base->hasClassWithFQSEN($class_fqsen)) { throw new CodeBaseException("Can't find class {$class_fqsen}"); } $class = $code_base->getClassByFQSEN($class_fqsen); return $class; }
/** * Visit a node with kind `\ast\AST_STATIC_CALL` * * @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 visitStaticCall(Node $node) : UnionType { $class_name = AST::classNameFromNode($this->context, $this->code_base, $node); // assert(!empty($class_name), 'Class name cannot be empty'); if (!$class_name) { return new UnionType(); } $method_name = $node->children['method']; // Give up on any complicated nonsense where the // method name is a variable such as in // `$variable->$function_name()`. if ($method_name instanceof Node) { return new UnionType(); } // Method names can some times turn up being // other method calls. assert(is_string($method_name), "Method name must be a string. Something else given."); $method_fqsen = FullyQualifiedMethodName::make(FullyQualifiedClassName::fromStringInContext($class_name, $this->context), $method_name); if (!$this->code_base->hasMethod($method_fqsen)) { return new UnionType(); } $method = $this->code_base->getMethod($method_fqsen); return $method->getUnionType(); }
/** * @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 string * The class name represented by the given call */ public function visitMethodCall(Node $node) : string { if ($node->children['expr']->kind == \ast\AST_VAR) { if ($node->children['expr']->children['name'] instanceof Node) { return ''; } // $var->method() if ($node->children['expr']->children['name'] == 'this') { if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, 'Using $this when not in object context', $this->context->getFile(), $node->lineno); return ''; } return (string) $this->context->getClassFQSEN(); } $variable_name = $node->children['expr']->children['name']; if (!$this->context->getScope()->hasVariableWithName($variable_name)) { // Got lost, couldn't find the variable in the current scope // If it really isn't defined, it will be caught by the // undefined var error return ''; } $variable = $this->context->getScope()->getVariableWithName($variable_name); // Hack - loop through the possible types of the var and assume // first found class is correct foreach ($variable->getUnionType()->nonGenericArrayTypes()->getTypeList() as $type) { $child_class_fqsen = FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); if ($this->code_base->hasClassWithFQSEN($child_class_fqsen)) { return (string) FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); } } // Could not find name return ''; } if ($node->children['expr']->kind == \ast\AST_PROP) { $prop = $node->children['expr']; if (!($prop->children['expr']->kind == \ast\AST_VAR && !$prop->children['expr']->children['name'] instanceof Node)) { return ''; } // $var->prop->method() $var = $prop->children['expr']; if ($var->children['name'] == 'this') { // If we're not in a class scope, 'this' won't work if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, 'Using $this when not in object context', $this->context->getFile(), $node->lineno); return ''; } // Get the class in scope $clazz = $this->code_base->getClassByFQSEN($this->context->getClassFQSEN()); if ($prop->children['prop'] instanceof Node) { // $this->$prop->method() - too dynamic, give up return ''; } $property_name = $prop->children['prop']; if ($clazz->hasPropertyWithName($this->code_base, $property_name)) { try { $property = $clazz->getPropertyByNameInContext($this->code_base, $property_name, $this->context); } catch (AccessException $exception) { Log::err(Log::EACCESS, $exception->getMessage(), $this->context->getFile(), $node->lineno); return ''; } // Find the first viable property type foreach ($property->getUnionType()->nonGenericArrayTypes()->getTypeList() as $type) { $class_fqsen = FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); if ($this->code_base->hasClassWithFQSEN($class_fqsen)) { return (string) $class_fqsen; } } } // No such property was found, or none were classes // that could be found return ''; } return ''; } if ($node->children['expr']->kind == \ast\AST_METHOD_CALL) { // Get the type returned by the first method // call. $union_type = UnionType::fromNode($this->context, $this->code_base, $node->children['expr']); // Find the subset of types that are viable // classes $viable_class_types = $union_type->nonNativeTypes()->nonGenericArrayTypes(); // If there are no non-native types, give up if ($viable_class_types->isEmpty()) { return ''; } // Return the first non-native type in the // list and hope its a class return (string) $viable_class_types->head(); } return ''; }
/** * @param CodeBase $code_base * A reference to the entire code base in which this * context exists * * @param ReflectionClass $class * A reflection class representing a builtin class. * * @return Clazz * A Class structural element representing the given named * builtin. */ public static function fromReflectionClass(CodeBase $code_base, \ReflectionClass $class) : Clazz { // Build a set of flags based on the constitution // of the built-in class $flags = 0; if ($class->isFinal()) { $flags = \ast\flags\CLASS_FINAL; } elseif ($class->isInterface()) { $flags = \ast\flags\CLASS_INTERFACE; } elseif ($class->isTrait()) { $flags = \ast\flags\CLASS_TRAIT; } if ($class->isAbstract()) { $flags |= \ast\flags\CLASS_ABSTRACT; } $context = new Context(); $class_fqsen = FullyQualifiedClassName::fromStringInContext($class->getName(), $context); // Build a base class element $clazz = new Clazz($context, $class->getName(), UnionType::fromStringInContext($class->getName(), $context), $flags, $class_fqsen); // If this class has a parent class, add it to the // class info if ($parent_class = $class->getParentClass()) { $parent_class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString('\\' . $parent_class->getName()); $parent_type = $parent_class_fqsen->asType(); $clazz->setParentType($parent_type); } // n.b.: public properties on internal classes don't get // listed via reflection until they're set unless // they have a default value. Therefore, we don't // bother iterating over `$class->getProperties()` // `$class->getStaticProperties()`. foreach ($class->getDefaultProperties() as $name => $value) { $property_context = $context->withScope(new ClassScope(new GlobalScope(), $clazz->getFQSEN())); $property_fqsen = FullyQualifiedPropertyName::make($clazz->getFQSEN(), $name); $property = new Property($property_context, $name, Type::fromObject($value)->asUnionType(), 0, $property_fqsen); $clazz->addProperty($code_base, $property, new None()); } foreach (UnionType::internalPropertyMapForClassName($clazz->getName()) as $property_name => $property_type_string) { $property_context = $context->withScope(new ClassScope(new GlobalScope(), $clazz->getFQSEN())); $property_type = UnionType::fromStringInContext($property_type_string, new Context()); $property_fqsen = FullyQualifiedPropertyName::make($clazz->getFQSEN(), $property_name); $property = new Property($property_context, $property_name, $property_type, 0, $property_fqsen); $clazz->addProperty($code_base, $property, new None()); } foreach ($class->getInterfaceNames() as $name) { $clazz->addInterfaceClassFQSEN(FullyQualifiedClassName::fromFullyQualifiedString('\\' . $name)); } foreach ($class->getTraitNames() as $name) { $clazz->addTraitFQSEN(FullyQualifiedClassName::fromFullyQualifiedString('\\' . $name)); } foreach ($class->getConstants() as $name => $value) { $constant_fqsen = FullyQualifiedClassConstantName::make($clazz->getFQSEN(), $name); $constant = new ClassConstant($context, $name, Type::fromObject($value)->asUnionType(), 0, $constant_fqsen); $clazz->addConstant($code_base, $constant); } foreach ($class->getMethods() as $reflection_method) { $method_context = $context->withScope(new ClassScope(new GlobalScope(), $clazz->getFQSEN())); $method_list = FunctionFactory::methodListFromReflectionClassAndMethod($method_context, $code_base, $class, $reflection_method); foreach ($method_list as $method) { $clazz->addMethod($code_base, $method, new None()); } } return $clazz; }
/** * @param CodeBase $code_base * A reference to the entire code base in which this * context exists * * @param ReflectionClass $class * A reflection class representing a builtin class. * * @return Clazz * A Class structural element representing the given named * builtin. */ public static function fromReflectionClass(CodeBase $code_base, \ReflectionClass $class) : Clazz { // Build a set of flags based on the constitution // of the built-in class $flags = 0; if ($class->isFinal()) { $flags = \ast\flags\CLASS_FINAL; } elseif ($class->isInterface()) { $flags = \ast\flags\CLASS_INTERFACE; } elseif ($class->isTrait()) { $flags = \ast\flags\CLASS_TRAIT; } if ($class->isAbstract()) { $flags |= \ast\flags\CLASS_ABSTRACT; } $context = new Context(); // Build a base class element $clazz = new Clazz($context, $class->getName(), UnionType::fromStringInContext($class->getName(), $context), $flags); $clazz->setFQSEN(FullyQualifiedClassName::fromStringInContext($class->getName(), $context)); // If this class has a parent class, add it to the // class info if ($parent_class = $class->getParentClass()) { $parent_class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString('\\' . $parent_class->getName()); $clazz->setParentClassFQSEN($parent_class_fqsen); } foreach ($class->getDefaultProperties() as $name => $value) { // TODO: whats going on here? $reflection_property = new \ReflectionProperty($class->getName(), $name); $property_context = $context->withClassFQSEN($clazz->getFQSEN()); $property = new Property($property_context, $name, Type::fromObject($value)->asUnionType(), 0); $property->setFQSEN(FullyQualifiedPropertyName::make($clazz->getFQSEN(), $name)); $clazz->addProperty($code_base, $property); } foreach ($class->getInterfaceNames() as $name) { $clazz->addInterfaceClassFQSEN(FullyQualifiedClassName::fromFullyQualifiedString('\\' . $name)); } foreach ($class->getTraitNames() as $name) { $clazz->addTraitFQSEN(FullyQualifiedClassName::fromFullyQualifiedString('\\' . $name)); } foreach ($class->getConstants() as $name => $value) { $constant = new ClassConstant($context, $name, Type::fromObject($value)->asUnionType(), 0); $constant->setFQSEN(FullyQualifiedClassConstantName::make($clazz->getFQSEN(), $name)); $clazz->addConstant($code_base, $constant); } foreach ($class->getMethods() as $reflection_method) { $method_list = FunctionFactory::methodListFromReflectionClassAndMethod($context->withClassFQSEN($clazz->getFQSEN()), $code_base, $class, $reflection_method); foreach ($method_list as $method) { $clazz->addMethod($code_base, $method); } } return $clazz; }