Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/main/php/lang.base.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,6 @@ function newinstance($spec, $args, $def= null) {
}

if ($generic) {
\lang\XPClass::detailsForClass($name);
xp::$meta[$name]['class'][DETAIL_GENERIC]= $generic;
}

Expand Down
209 changes: 209 additions & 0 deletions src/main/php/lang/ClassMeta.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<?php namespace lang;

use ReflectionClass;

/** @test lang.unittest.ClassMetaTest */
class ClassMeta {

/**
* Returns the comment text
*
* @param string $comment
* @param ?int $p
* @return string
*/
private function comment($comment, $p= null) {
return trim(preg_replace('/\n\s+\* ?/', "\n", "\n".substr(
$comment,
3, // "/**[ \n]"
($p ?? strpos($comment, '* @')) - 2 // position of first details token
)));
}

/**
* Returns position of matching closing brace, or the string's length
* if no closing / opening brace is found.
*
* @param string $input
* @param string $open
* @param string $close
* @param int
*/
private function matching($input, $open, $close) {
for ($braces= $open.$close, $i= 0, $b= 0, $s= strlen($input); $i < $s; $i+= strcspn($input, $braces, $i)) {
if ($input[$i] === $open) {
$b++;
} else if ($input[$i] === $close) {
if (0 === --$b) return $i + 1;
}
$i++;
}
return $i;
}

/**
* Extracts type from a comment
*
* @param string $comment
* @param ReflectionClass $reflect
* @return string
*/
private function type($comment, $reflect= []) {
if (0 === strncmp($comment, 'function(', 9)) {
$p= $this->matching($comment, '(', ')');
$p+= strspn($comment, ': ', $p);
return substr($comment, 0, $p).$this->type(substr($comment, $p), $reflect);
} else if (0 === strncmp($comment, '(function(', 10)) {
$p= $this->matching($comment, '(', ')');
return substr($comment, 0, $p).$this->type(substr($comment, $p), $reflect);
} else if ('[' === $comment[0]) {
$p= $this->matching($comment, '[', ']');
return substr($comment, 0, $p);
} else if (strstr($comment, '<')) {
$p= $this->matching($comment, '<', '>');
$type= substr($comment, 0, $p);
} else {
$type= substr($comment, 0, strcspn($comment, ' '));
}

if ('\\' === ($type[0] ?? null)) {
return strtr(substr($type, 1), '\\', '.');
} else {
return $type;
}
}

/**
* Returns imports used in the class file the given class was declared in
*
* @param ReflectionClass $reflect
* @return [:string]
*/
public function imports($reflect) {
static $break= [T_CLASS => true, T_INTERFACE => true, T_TRAIT => true, 372 /* T_ENUM */ => true];
static $types= [T_WHITESPACE => true, 44 => true, 59 => true, 123 => true];

// Exclude classes declared inside eval(), their declaration is not accessible
$file= $reflect->getFileName();
if (false !== strpos($file, ': eval')) return [];

$tokens= PhpToken::tokenize(file_get_contents($file));
$imports= [];
for ($i= 0, $s= sizeof($tokens); $i < $s; $i++) {
if (isset($break[$tokens[$i]->id])) break;
if (T_USE !== $tokens[$i]->id) continue;

do {
$type= '';
for ($i+= 2; $i < $s, !isset($types[$tokens[$i]->id]); $i++) {
$type.= $tokens[$i]->text;
}

// Skip over whitespace
if (T_WHITESPACE === $tokens[$i]->id) $i++;

// use `lang\{Type, Primitive as P}` vs. `use lang\Primitive as P;` vs. `use lang\Primitive`
if (123 === $tokens[$i]->id) {
$alias= null;
$group= '';
for ($i+= 1; $i < $s; $i++) {
if (44 === $tokens[$i]->id) {
$imports[$alias ?? $group]= $type.$group;
$alias= null;
$group= '';
} else if (125 === $tokens[$i]->id) {
$imports[$alias ?? $group]= $type.$group;
break;
} else if (T_AS === $tokens[$i]->id) {
$i+= 2;
$alias= $tokens[$i]->text;
} else if (T_WHITESPACE !== $tokens[$i]->id) {
$group.= $tokens[$i]->text;
}
}
} else if (T_AS === $tokens[$i]->id) {
$i+= 2;
$imports[$tokens[$i]->text]= $type;
} else if (false === ($p= strrpos($type, '\\'))) {
$imports[$type]= null;
} else {
$imports[substr($type, strrpos($type, '\\') + 1)]= $type;
}

// Skip over whitespace
if (T_WHITESPACE === $tokens[$i]->id) $i++;
} while (44 === $tokens[$i]->id);
}
return $imports;
}

/**
* Returns class meta information for a given class
*
* @param string|ReflectionClass|object $class
* @return [:var]
*/
public function meta($class) {
if ($class instanceof ReflectionClass) {
$reflect= $class;
} else if (is_object($class)) {
$reflect= new ReflectionClass($class);
} else {
$reflect= new ReflectionClass(strtr($class, '.', '\\'));
}

$properties= [];
foreach ($reflect->getProperties() as $property) {
$comment= $property->getDocComment() ?: '';
$type= null;
if (false !== ($p= strpos($comment, '* @'))) {
preg_match_all('/@([a-z]+)\s*([^\r\n]+)?/', $comment, $matches, PREG_SET_ORDER, $p + 2);
foreach ($matches as $match) {
if ('type' === $match[1]) {
$type= $this->type($match[2], $reflect);
}
}
}

$properties[$property->name]= [
DETAIL_RETURNS => $type,
DETAIL_COMMENT => $this->comment($comment, $p),
];
}

$methods= [];
foreach ($reflect->getMethods() as $method) {
$comment= $method->getDocComment() ?: '';
$params= $throws= [];
$returns= null;

// Parse doc comment
if (false !== ($p= strpos($comment, '* @'))) {
preg_match_all('/@([a-z]+)\s*([^\r\n]+)?/', $comment, $matches, PREG_SET_ORDER, $p + 2);
foreach ($matches as $match) {
if ('param' === $match[1]) {
$params[]= $this->type($match[2], $reflect);
} else if ('return' === $match[1]) {
$returns= $this->type($match[2], $reflect);
} else if ('throws' === $match[1]) {
$throws[]= $this->type($match[2], $reflect);
}
}
}

$methods[$method->name]= [
DETAIL_ARGUMENTS => $params,
DETAIL_RETURNS => $returns,
DETAIL_THROWS => $throws,
DETAIL_COMMENT => $this->comment($comment, $p),
];
}

// Returns structure compatible with xp::$meta
return [
'class' => [DETAIL_COMMENT => $this->comment($reflect->getDocComment() ?: '')],
$properties,
$methods,
];
}
}
15 changes: 8 additions & 7 deletions src/main/php/lang/FunctionType.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,17 @@ public function literal(): string {
* @param php.ReflectionFunctionAbstract $value
* @param [lang.Type] $signature
* @param function(string): var $value A function to invoke when verification fails
* @param php.ReflectionClass $class Class to get details from, optionally
* @param php.ReflectionClass $reflect Class to get details from, optionally
* @return var
*/
protected function verify($r, $signature, $false, $class= null) {
if ($class) {
$details= XPClass::detailsForClass(XPClass::nameOf($class->name))[1][$r->name] ?? null;
protected function verify($r, $signature, $false, $reflect= null) {
if ($reflect) {
$class= new XPClass($reflect);
$details= $class->meta()[1][$r->name] ?? null;
$resolve= [
'static' => fn() => new XPClass($class),
'self' => fn() => new XPClass($class),
'parent' => fn() => new XPClass($class->getParentClass()),
'static' => fn() => $class,
'self' => fn() => $class,
'parent' => fn() => new XPClass($reflect->getParentClass()),
];
} else {
$details= null;
Expand Down
66 changes: 31 additions & 35 deletions src/main/php/lang/GenericTypes.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,22 @@ public function newType(XPClass $base, array $arguments) {
* @return string created type's literal
*/
public function newType0($base, $arguments) {

// Verify
$details= XPClass::detailsForClass($base->getName());
$annotations= $details ? $details['class'][DETAIL_ANNOTATIONS] : [];
if (!isset($annotations['generic']['self'])) {
throw new IllegalStateException('Class '.$base->name.' is not a generic definition');
$reflect= $base->reflect();
$generic= $reflect->getAttributes(Generic::class);
if (empty($generic) || (($annotated= $generic[0]->getArguments()) && !isset($annotated['self']))) {
throw new IllegalStateException('Class '.$base->name.' is not a generic definition');
}

// Generic(self: 'K, V') => ["K", "V"]
$components= [];
foreach (Type::split($annotations['generic']['self']) as $cs => $name) {
foreach (Type::split($annotated['self']) as $name) {
$components[]= ltrim($name);
}
$cs++;
if ($cs !== sizeof($arguments)) {
if (sizeof($components) !== sizeof($arguments)) {
throw new IllegalArgumentException(sprintf(
'Class %s expects %d component(s) <%s>, %d argument(s) given',
$base->name,
$cs,
sizeof($components),
implode(', ', $components),
sizeof($arguments)
));
Expand All @@ -59,7 +58,8 @@ public function newType0($base, $arguments) {

// Create class if it doesn't exist yet
if (!class_exists($name, false) && !interface_exists($name, false) && !trait_exists($name, false) && !enum_exists($name, false)) {
$meta= \xp::$meta[$base->name];
$meta= $base->meta();
unset(\xp::$meta[$base->name]);

// Parse placeholders into a lookup map
$placeholders= [];
Expand Down Expand Up @@ -141,9 +141,9 @@ public function newType0($base, $arguments) {
$parent.= $tokens[$i][1];
}
$i--;
if (isset($annotations['generic']['parent'])) {
if (isset($annotated['parent'])) {
$xargs= [];
foreach (Type::split($annotations['generic']['parent']) as $j => $placeholder) {
foreach (Type::split($annotated['parent']) as $j => $placeholder) {
$xargs[]= Type::forName(strtr(ltrim($placeholder), $placeholders));
}
$src.= ' extends \\'.$this->newType0(new XPClass($base->reflect()->getParentClass()), $xargs);
Expand All @@ -153,7 +153,7 @@ public function newType0($base, $arguments) {
} else if (T_IMPLEMENTS === $tokens[$i][0]) {
$src.= ' implements';
$counter= 0;
$annotation= $annotations['generic']['implements'] ?? null;
$annotation= $annotated['implements'] ?? null;
array_unshift($state, T_CLASS);
array_unshift($state, 5);
} else if ('{' === $tokens[$i][0]) {
Expand All @@ -167,7 +167,7 @@ public function newType0($base, $arguments) {
if (T_EXTENDS === $tokens[$i][0]) {
$src.= ' extends';
$counter= 0;
$annotation= $annotations['generic']['extends'] ?? null;
$annotation= $annotated['extends'] ?? null;
array_unshift($state, T_INTERFACE);
array_unshift($state, 5);
} else if ('{' === $tokens[$i][0]) {
Expand All @@ -184,12 +184,13 @@ public function newType0($base, $arguments) {
array_unshift($state, 2);
$m= $tokens[$i+ 2][1];
$p= 0;
$annotations= [$meta[1][$m][DETAIL_ANNOTATIONS] ?? [], $meta[1][$m][DETAIL_TARGET_ANNO] ?? []];
$generic= $reflect->getMethod($m)->getAttributes(Generic::class);
} else if (T_VARIABLE === $tokens[$i][0]) {
$f= substr($tokens[$i][1], 1);
$annotations= $meta[0][$f][DETAIL_ANNOTATIONS] ?? [];
if (isset($annotations['generic']['var'])) {
$meta[0][$f][DETAIL_RETURNS]= strtr($annotations['generic']['var'], $placeholders);
$generic= $reflect->getProperty($f)->getAttributes(Generic::class);
$annotations= $generic ? $generic[0]->getArguments() : [];
if (isset($annotations['var'])) {
$meta[0][$f][DETAIL_RETURNS]= strtr($annotations['var'], $placeholders);
}
} else if ('}' === $tokens[$i][0]) {
$src.= '}';
Expand Down Expand Up @@ -219,32 +220,31 @@ public function newType0($base, $arguments) {
$default[$p].= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i];
}
} else if (3 === $state[0]) { // Method body
if (';' === $tokens[$i][0]) {
// Abstract method
if (isset($annotations[0]['generic']['return'])) {
$meta[1][$m][DETAIL_RETURNS]= strtr($annotations[0]['generic']['return'], $placeholders);
if (';' === $tokens[$i][0]) { // Abstract method
$annotations= $generic ? $generic[0]->getArguments() : [];
if (isset($annotations['return'])) {
$meta[1][$m][DETAIL_RETURNS]= strtr($annotations['return'], $placeholders);
}
if (isset($annotations[0]['generic']['params'])) {
foreach (Type::split($annotations[0]['generic']['params']) as $j => $placeholder) {
if (isset($annotations['params'])) {
foreach (Type::split($annotations['params']) as $j => $placeholder) {
if ('' !== ($replaced= strtr($placeholder, $placeholders))) {
$meta[1][$m][DETAIL_ARGUMENTS][$j]= $replaced;
}
}
}
$annotations= [];
unset($meta[1][$m][DETAIL_ANNOTATIONS]['generic']);
array_shift($state);
} else if ('{' === $tokens[$i][0]) {
$braces= 1;
array_shift($state);
array_unshift($state, 4);
$src.= '{';
if (isset($annotations[0]['generic']['return'])) {
$meta[1][$m][DETAIL_RETURNS]= strtr($annotations[0]['generic']['return'], $placeholders);
$annotations= $generic ? $generic[0]->getArguments() : [];
if (isset($annotations['return'])) {
$meta[1][$m][DETAIL_RETURNS]= strtr($annotations['return'], $placeholders);
}
if (isset($annotations[0]['generic']['params'])) {
if (isset($annotations['params'])) {
$generic= [];
foreach (Type::split($annotations[0]['generic']['params']) as $j => $placeholder) {
foreach (Type::split($annotations['params']) as $j => $placeholder) {
if ('' === ($replaced= strtr($placeholder, $placeholders))) {
$generic[$j]= null;
} else {
Expand Down Expand Up @@ -274,9 +274,6 @@ public function newType0($base, $arguments) {
}
}
}

$annotations= [];
unset($meta[1][$m][DETAIL_ANNOTATIONS]['generic']);
continue;
}
} else if (4 === $state[0]) { // Method body
Expand Down Expand Up @@ -337,7 +334,6 @@ public function newType0($base, $arguments) {
}
method_exists($name, '__static') && $name::__static();
}
unset($meta['class'][DETAIL_ANNOTATIONS]['generic']);
\xp::$meta[$qname]= $meta;
\xp::$cn[$name]= $qname;
}
Expand Down
Loading
Loading