public function testMethodInCodeBase() { $context = $this->contextForCode("\n namespace A;\n Class B {\n public function c() {\n return 42;\n }\n }\n "); $class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString('\\A\\b'); self::assertTrue($this->code_base->hasClassWithFQSEN($class_fqsen), "Class with FQSEN {$class_fqsen} not found"); $clazz = $this->code_base->getClassByFQSEN($class_fqsen); self::assertTrue($clazz->hasMethodWithName($this->code_base, 'c'), "Method with FQSEN not found"); }
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'); }
public function testSimple() { $context = new Context(); $context_namespace = $context->withNamespace('\\A'); $context_class = $context_namespace->withClassFQSEN(FullyQualifiedClassName::fromFullyQualifiedString('\\A\\B')); $context_method = $context_namespace->withMethodFQSEN(FullyQualifiedMethodName::fromFullyQualifiedString('\\A\\b::c')); $this->assertTrue(!empty($context)); $this->assertTrue(!empty($context_namespace)); $this->assertTrue(!empty($context_class)); $this->assertTrue(!empty($context_method)); }
public function testSimple() { $context = new Context(); $context_namespace = $context->withNamespace('\\A'); $context_class = $context_namespace->withScope(new ClassScope($context_namespace->getScope(), FullyQualifiedClassName::fromFullyQualifiedString('\\A\\B'))); $context_method = $context_namespace->withScope(new FunctionLikeScope($context_namespace->getScope(), FullyQualifiedMethodName::fromFullyQualifiedString('\\A\\b::c'))); $this->assertTrue(!empty($context)); $this->assertTrue(!empty($context_namespace)); $this->assertTrue(!empty($context_class)); $this->assertTrue(!empty($context_method)); }
/** * @param $fully_qualified_string * An FQSEN string like '\Namespace\Class::methodName' */ public static function fromFullyQualifiedString(string $fully_qualified_string) { assert(false !== strpos($fully_qualified_string, '::'), "Fully qualified class element lacks '::' delimiter in {$fully_qualified_string}."); list($fully_qualified_class_name_string, $name_string) = explode('::', $fully_qualified_string); $fully_qualified_class_name = FullyQualifiedClassName::fromFullyQualifiedString($fully_qualified_class_name_string); // Split off the alternate ID $parts = explode(',', $name_string); $name = $parts[0]; $alternate_id = (int) ($parts[1] ?? 0); assert(is_int($alternate_id), "Alternate must be an integer in {$fully_qualified_string}"); return static::make($fully_qualified_class_name, $name, $alternate_id); }
/** * @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); return new Clazz($clazz); }
public static function createSchema() : Schema { $schema = new Schema('File', [new Column('file_path', Column::TYPE_STRING, true), new Column('modification_time', Column::TYPE_INT)]); $schema->addAssociation(new ListAssociation('FileClassFQSEN', Column::TYPE_STRING, function (File $file, array $class_fqsen_string_list) { $file->getFile()->setClassFQSENList(array_map(function (string $fqsen_string) { return FullyQualifiedClassName::fromFullyQualifiedString($fqsen_string); }, $class_fqsen_string_list)); }, function (File $file) { return array_map(function (FullyQualifiedClassName $fqsen) { return (string) $fqsen; }, $file->getFile()->getClassFQSENList()); })); $schema->addAssociation(new ListAssociation('FileMethodFQSEN', Column::TYPE_STRING, function (File $file, array $method_fqsen_string_list) { $file->getFile()->setMethodFQSENList(array_map(function (string $fqsen_string) { if (false !== strpos($fqsen_string, '::')) { return FullyQualifiedMethodName::fromFullyQualifiedString($fqsen_string); } else { return FullyQualifiedFunctionName::fromFullyQualifiedString($fqsen_string); } }, $method_fqsen_string_list)); }, function (File $file) { return array_map(function (FQSEN $fqsen) { return (string) $fqsen; }, $file->getFile()->getMethodFQSENList()); })); $schema->addAssociation(new ListAssociation('FilePropertyFQSEN', Column::TYPE_STRING, function (File $file, array $fqsen_string_list) { $file->getFile()->setPropertyFQSENList(array_map(function (string $fqsen_string) { if (false !== strpos($fqsen_string, '::')) { return FullyQualifiedPropertyName::fromFullyQualifiedString($fqsen_string); } else { return FullyQualifiedFunctionName::fromFullyQualifiedString($fqsen_string); } }, $fqsen_string_list)); }, function (File $file) { return array_map(function (FQSEN $fqsen) { return (string) $fqsen; }, $file->getFile()->getPropertyFQSENList()); })); $schema->addAssociation(new ListAssociation('FileConstantFQSEN', Column::TYPE_STRING, function (File $file, array $fqsen_string_list) { $file->getFile()->setConstantFQSENList(array_map(function (string $fqsen_string) { if (false !== strpos($fqsen_string, '::')) { return FullyQualifiedConstantName::fromFullyQualifiedString($fqsen_string); } else { return FullyQualifiedFunctionName::fromFullyQualifiedString($fqsen_string); } }, $fqsen_string_list)); }, function (File $file) { return array_map(function (FQSEN $fqsen) { return (string) $fqsen; }, $file->getFile()->getConstantFQSENList()); })); return $schema; }
/** * @param $fully_qualified_string * An FQSEN string like '\Namespace\Class::methodName' */ public static function fromFullyQualifiedString(string $fully_qualified_string) { assert(false !== strpos($fully_qualified_string, '::'), "Fully qualified class element lacks '::' delimiter"); list($fully_qualified_class_name_string, $name_string) = explode('::', $fully_qualified_string); $fully_qualified_class_name = FullyQualifiedClassName::fromFullyQualifiedString($fully_qualified_class_name_string); // Make sure that we're actually getting a class // name reference back assert($fully_qualified_class_name instanceof FullyQualifiedClassName, "FQSEN must be an instanceof FullyQualifiedClassName"); // Split off the alternate ID $parts = explode(',', $name_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); }
/** * @return array * A map from alias to target */ private function aliasTargetMapFromUseNode(Node $node, string $prefix = '') : array { assert($node->kind == \ast\AST_USE, 'Method takes AST_USE nodes'); $map = []; foreach ($node->children ?? [] as $child_node) { $target = $child_node->children['name']; if (empty($child_node->children['alias'])) { if (($pos = strrpos($target, '\\')) !== false) { $alias = substr($target, $pos + 1); } else { $alias = $target; } } else { $alias = $child_node->children['alias']; } if ($node->flags == T_FUNCTION) { $parts = explode('\\', $target); $function_name = array_pop($parts); $target = FullyQualifiedFunctionName::make(implode('\\', $parts), $function_name); } else { $target = FullyQualifiedClassName::fromFullyQualifiedString($prefix . '\\' . $target); } $map[$alias] = [$child_node->flags, $target]; } return $map; }
/** * @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; } else { if ($class->isInterface()) { $flags = \ast\flags\CLASS_INTERFACE; } else { if ($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); // 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 = new Property($context->withClassFQSEN($clazz->getFQSEN()), $name, Type::fromObject($value)->asUnionType(), 0); $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) { $clazz->addConstant($code_base, new Constant($context, $name, Type::fromObject($value)->asUnionType(), 0)); } foreach ($class->getMethods() as $reflection_method) { $method_list = Method::methodListFromReflectionClassAndMethod($context->withClassFQSEN($clazz->getFQSEN()), $code_base, $class, $reflection_method); foreach ($method_list as $method) { $clazz->addMethod($code_base, $method); } } return $clazz; }
/** * @return array * A map from alias to target */ private function aliasTargetMapFromUseNode(Node $node, string $prefix = '') : array { assert($node->kind == \ast\AST_USE, 'Method takes AST_USE nodes'); $map = []; foreach ($node->children ?? [] as $child_node) { $target = $child_node->children['name']; if (empty($child_node->children['alias'])) { if (($pos = strrpos($target, '\\')) !== false) { $alias = substr($target, $pos + 1); } else { $alias = $target; } } else { $alias = $child_node->children['alias']; } // if AST_USE does not have any flags set, then its AST_USE_ELEM // children will (this will be for AST_GROUP_USE) if ($node->flags !== 0) { $target_node = $node; } else { $target_node = $child_node; } if ($target_node->flags == T_FUNCTION) { $parts = explode('\\', $target); $function_name = array_pop($parts); $target = FullyQualifiedFunctionName::make($prefix . '\\' . implode('\\', $parts), $function_name); } else { if ($target_node->flags == T_CONST) { $parts = explode('\\', $target); $name = array_pop($parts); $target = FullyQualifiedGlobalConstantName::make($prefix . '\\' . implode('\\', $parts), $name); } else { $target = FullyQualifiedClassName::fromFullyQualifiedString($prefix . '\\' . $target); } } $map[$alias] = [$target_node->flags, $target]; } return $map; }
/** * @return FullyQualifiedGlobalStructuralElement * The namespace mapped name for the given flags and name */ public function getNamespaceMapFor(int $flags, string $name) : FullyQualifiedGlobalStructuralElement { $name = strtolower($name); // Look for the mapping on the part before a // slash $name_parts = explode('\\', $name, 2); $suffix = ''; if (count($name_parts) > 1) { $name = $name_parts[0]; $suffix = $name_parts[1]; } assert(!empty($this->namespace_map[$flags][$name]), "No namespace defined for name"); assert($this->namespace_map[$flags][$name] instanceof FQSEN, "Namespace map was not an FQSEN"); $fqsen = $this->namespace_map[$flags][$name]; if (!$suffix) { return $fqsen; } switch ($flags) { case \ast\flags\USE_NORMAL: return FullyQualifiedClassName::fromFullyQualifiedString((string) $fqsen . '\\' . $suffix); case \ast\flags\USE_FUNCTION: return FullyQualifiedFunctionName::fromFullyQualifiedString((string) $fqsen . '\\' . $suffix); } assert(false, "Unknown flag {$flags}"); return $fqsen; }
/** * @return void */ public function unserialize($serialized) { list($file_ref, $serialized) = explode('^', $serialized); parent::unserialize($file_ref); list($namespace, $class_fqsen, $method_fqsen, $closure_fqsen) = explode('|', $serialized); $this->namespace = $namespace; $this->class_fqsen = $class_fqsen ? FullyQualifiedClassName::fromFullyQualifiedString($class_fqsen) : null; // Determine if we have a method or a function if (false === strpos($method_fqsen, '::')) { $this->method_fqsen = $method_fqsen ? FullyQualifiedFunctionName::fromFullyQualifiedString($method_fqsen) : null; } else { $this->method_fqsen = $method_fqsen ? FullyQualifiedMethodName::fromFullyQualifiedString($method_fqsen) : null; } $this->closure_fqsen = $closure_fqsen ? FullyQualifiedFunctionName::fromFullyQualifiedString($closure_fqsen) : null; }
/** * @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; }
/** * 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; }
public function unserialize($serialized) { list($file_ref, $serialized) = explode('^', $serialized); parent::unserialize($file_ref); list($namespace, $is_conditional, $class_fqsen, $method_fqsen, $closure_fqsen) = explode('|', $serialized); $this->namespace = $namespace; $this->is_conditional = (bool) $is_conditional; $this->class_fqsen = $class_fqsen ? FullyQualifiedClassName::fromFullyQualifiedString($class_fqsen) : null; $this->method_fqsen = $method_fqsen ? FullyQualifiedMethodName::fromFullyQualifiedString($method_fqsen) : null; $this->closure_fqsen = $closure_fqsen ? FullyQualifiedFunctionName::fromFullyQualifiedString($closure_fqsen) : null; }
/** * 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; }