/** * @return FullyQualifiedPropertyName * The fully-qualified structural element name of this * structural element */ public function getFQSEN() : FQSEN { // Get the stored FQSEN if it exists if ($this->fqsen) { return $this->fqsen; } return FullyQualifiedPropertyName::fromStringInContext($this->getName(), $this->getContext()); }
public function testFullyQualifiedPropertyName() { $this->assertFQSENEqual(FullyQualifiedPropertyName::make(FullyQualifiedClassName::make('\\Name\\Space', 'a'), 'p'), '\\Name\\Space\\a::p'); $this->assertFQSENEqual(FullyQualifiedPropertyName::fromFullyQualifiedString('\\Name\\a::p'), '\\Name\\a::p'); $this->assertFQSENEqual(FullyQualifiedPropertyName::fromFullyQualifiedString('Name\\a::p'), '\\Name\\a::p'); $this->assertFQSENEqual(FullyQualifiedPropertyName::fromFullyQualifiedString('\\Name\\Space\\a::p,2'), '\\Name\\Space\\a::p,2'); $this->assertFQSENEqual(FullyQualifiedPropertyName::fromFullyQualifiedString('\\Name\\Space\\a,1::p,2'), '\\Name\\Space\\a,1::p,2'); $this->assertFQSENEqual(FullyQualifiedPropertyName::fromStringInContext('a::p', $this->context), '\\a::p'); }
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; }
/** * Visit a node with kind `\ast\AST_PROP_DECL` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitPropDecl(Node $node) : Context { // Bomb out if we're not in a class context $clazz = $this->getContextClass(); // Get a comment on the property declaration $comment = Comment::fromStringInContext($node->children[0]->docComment ?? '', $this->context); foreach ($node->children ?? [] as $i => $child_node) { // Ignore children which are not property elements if (!$child_node || $child_node->kind != \ast\AST_PROP_ELEM) { continue; } // If something goes wrong will getting the type of // a property, we'll store it as a future union // type and try to figure it out later $future_union_type = null; try { // Get the type of the default $union_type = UnionType::fromNode($this->context, $this->code_base, $child_node->children['default'], false); } catch (IssueException $exception) { $future_union_type = new FutureUnionType($this->code_base, $this->context, $child_node->children['default']); $union_type = new UnionType(); } // Don't set 'null' as the type if thats the default // given that its the default default. if ($union_type->isType(NullType::instance())) { $union_type = new UnionType(); } $property_name = $child_node->children['name']; assert(is_string($property_name), 'Property name must be a string. ' . 'Got ' . print_r($property_name, true) . ' at ' . $this->context); $property = new Property(clone $this->context->withLineNumberStart($child_node->lineno ?? 0), is_string($child_node->children['name']) ? $child_node->children['name'] : '_error_', $union_type, $node->flags ?? 0); $property->setFQSEN(FullyQualifiedPropertyName::make($clazz->getFQSEN(), $property->getName())); // Add the property to the class $clazz->addProperty($this->code_base, $property); $property->setSuppressIssueList($comment->getSuppressIssueList()); // Look for any @var declarations if ($variable = $comment->getVariableList()[$i] ?? null) { if ((string) $union_type != 'null' && !$union_type->canCastToUnionType($variable->getUnionType())) { $this->emitIssue(Issue::TypeMismatchProperty, $child_node->lineno ?? 0, (string) $union_type, (string) $property->getFQSEN(), (string) $variable->getUnionType()); } // Set the declared type to the doc-comment type and add // |null if the default value is null $property->getUnionType()->addUnionType($variable->getUnionType()); } // Wait until after we've added the (at)var type // before setting the future so that calling // $property->getUnionType() doesn't force the // future to be reified. if (!empty($future_union_type)) { $property->setFutureUnionType($future_union_type); } } return $this->context; }
/** * @return Property * A variable in scope or a new variable * * @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 given * class * * @throws TypeException * An exception may be thrown if the only viable candidate * is a non-class type. */ public function getOrCreateProperty(string $property_name) : Property { try { return $this->getProperty($property_name); } catch (IssueException $exception) { // Ignore it, because we'll create our own // property } catch (UnanalyzableException $exception) { // Ignore it, because we'll create our own // property } try { $class_list = (new ContextNode($this->code_base, $this->context, $this->node))->getClassList(); } catch (CodeBaseException $exception) { throw new IssueException(Issue::fromType(Issue::UndeclaredClassReference)($this->context->getFile(), $this->node->lineno ?? 0, [$exception->getFQSEN()])); } if (empty($class_list)) { throw new UnanalyzableException($this->node, "Could not get class name from node"); } $class = array_values($class_list)[0]; $flags = 0; if ($this->node->kind == \ast\AST_STATIC_PROP) { $flags |= \ast\flags\MODIFIER_STATIC; } // Otherwise, we'll create it $property = new Property($this->context, $property_name, new UnionType(), $flags); $property->setFQSEN(FullyQualifiedPropertyName::make($class->getFQSEN(), $property_name)); $class->addProperty($this->code_base, $property); return $property; }
/** * @param Node $node * A node that has a reference to a variable * * @param Context $context * The context in which we found the reference * * @param CodeBase $code_base * * @return Variable * A variable in scope or a new variable * * @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 given * class * * @throws TypeException * An exception may be thrown if the only viable candidate * is a non-class type. */ public static function getOrCreatePropertyFromNodeInContext(string $property_name, Node $node, Context $context, CodeBase $code_base) : Property { assert(is_string($property_name), 'Property name must be a string. ' . 'Got ' . print_r($property_name, true) . ' at ' . $context); // Figure out the class we're looking the property // up for $clazz = self::classFromNodeInContext($node, $context, $code_base); // Return it if the property exists on the class if ($clazz->hasPropertyWithName($code_base, $property_name)) { return $clazz->getPropertyByNameInContext($code_base, $property_name, $context); } $flags = 0; if ($node->kind == \ast\AST_STATIC_PROP) { $flags |= \ast\flags\MODIFIER_STATIC; } // Otherwise, we'll create it $property = new Property($context, $property_name, new UnionType(), $flags); $property->setFQSEN(FullyQualifiedPropertyName::make($clazz->getFQSEN(), $property_name)); $clazz->addProperty($code_base, $property); return $property; }
/** * @return Property * A variable in scope or a new variable * * @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 given * class * * @throws TypeException * An exception may be thrown if the only viable candidate * is a non-class type. */ public function getOrCreateProperty(string $property_name) : Property { try { return $this->getProperty($property_name); } catch (CodeBaseException $exception) { // Ignore it, because we'll create our own // property } catch (UnanalyzableException $exception) { // Ignore it, because we'll create our own // property } // Figure out the class we're looking the property // up for $class = $this->getClass(); $flags = 0; if ($this->node->kind == \ast\AST_STATIC_PROP) { $flags |= \ast\flags\MODIFIER_STATIC; } // Otherwise, we'll create it $property = new Property($this->context, $property_name, new UnionType(), $flags); $property->setFQSEN(FullyQualifiedPropertyName::make($class->getFQSEN(), $property_name)); $class->addProperty($this->code_base, $property); return $property; }
/** * @param string $name * The name of the property * * @param Context $context * The context of the caller requesting the property * * @return Property * A property with the given name * * @throws IssueException * An exception may be thrown if the caller does not * have access to the given property from the given * context */ public function getPropertyByNameInContext(CodeBase $code_base, string $name, Context $context) : Property { // Get the FQSEN of the property we're looking for $property_fqsen = FullyQualifiedPropertyName::make($this->getFQSEN(), $name); $property = null; // Figure out if we have the property $has_property = $code_base->hasPropertyWithFQSEN($property_fqsen); // Figure out if the property is accessible $is_property_accessible = false; if ($has_property) { $property = $code_base->getPropertyByFQSEN($property_fqsen); $is_remote_access = !$context->isInClassScope() || !$context->getClassInScope($code_base)->getUnionType()->canCastToExpandedUnionType($this->getUnionType(), $code_base); $is_property_accessible = !$is_remote_access || $property->isPublic(); } // If the property exists and is accessible, return it if ($is_property_accessible) { return $property; } // Check to see if we can use a __get magic method if ($this->hasMethodWithName($code_base, '__get')) { $method = $this->getMethodByName($code_base, '__get'); // Make sure the magic method is accessible if ($method->isPrivate()) { throw new IssueException(Issue::fromType(Issue::AccessPropertyPrivate)($context->getFile(), $context->getLineNumberStart(), [(string) $property_fqsen])); } else { if ($method->isProtected()) { throw new IssueException(Issue::fromType(Issue::AccessPropertyProtected)($context->getFile(), $context->getLineNumberStart(), [(string) $property_fqsen])); } } $property = new Property($context, $name, $method->getUnionType(), 0, $property_fqsen); $this->addProperty($code_base, $property, new None()); return $property; } else { if ($has_property) { // If we have a property, but its inaccessible, emit // an issue if ($property->isPrivate()) { throw new IssueException(Issue::fromType(Issue::AccessPropertyPrivate)($context->getFile(), $context->getLineNumberStart(), ["{$this->getFQSEN()}::\${$property->getName()}"])); } if ($property->isProtected()) { throw new IssueException(Issue::fromType(Issue::AccessPropertyProtected)($context->getFile(), $context->getLineNumberStart(), ["{$this->getFQSEN()}::\${$property->getName()}"])); } } } // Check to see if missing properties are allowed // or we're stdclass if (Config::get()->allow_missing_properties || $this->getFQSEN() == FullyQualifiedClassName::getStdClassFQSEN()) { $property = new Property($context, $name, new UnionType(), 0, $property_fqsen); $this->addProperty($code_base, $property, new None()); return $property; } throw new IssueException(Issue::fromType(Issue::UndeclaredProperty)($context->getFile(), $context->getLineNumberStart(), ["{$this->getFQSEN()}::\${$name}}"])); }
/** * @param string $name * The name of the property * * @param Context $context * The context of the caller requesting the property * * @return Property * A property with the given name * * @throws IssueException * An exception may be thrown if the caller does not * have access to the given property from the given * context */ public function getPropertyByNameInContext(CodeBase $code_base, string $name, Context $context) : Property { // Check to see if we have the property if (!$code_base->hasProperty($this->getFQSEN(), $name)) { // If we don't have the property but do have a // __get method, then we can create the property if ($this->hasMethodWithName($code_base, '__get')) { $property = new Property($context, $name, new UnionType(), 0); $property->setFQSEN(FullyQualifiedPropertyName::make($this->getFQSEN(), $name)); $this->addProperty($code_base, $property); } else { throw new IssueException(Issue::fromType(Issue::UndeclaredProperty)($context->getFile(), $context->getLineNumberStart(), ["{$this->getFQSEN()}::\${$name}}"])); } } $property = $code_base->getProperty($this->getFQSEN(), $name); // If we're getting the property from outside of this // class and the property isn't public and we don't // have a getter or setter, emit an access error if ((!$context->hasClassFQSEN() || $context->getClassFQSEN() != $this->getFQSEN()) && !$property->isPublic() && !$this->hasMethodWithName($code_base, '__get') && !$this->hasMethodWithName($code_base, '__set')) { if ($property->isPrivate()) { throw new IssueException(Issue::fromType(Issue::AccessPropertyPrivate)($context->getFile(), $context->getLineNumberStart(), ["{$this->getFQSEN()}::\${$property->getName()}"])); } if ($property->isProtected()) { throw new IssueException(Issue::fromType(Issue::AccessPropertyProtected)($context->getFile(), $context->getLineNumberStart(), ["{$this->getFQSEN()}::\${$property->getName()}"])); } } return $property; }