From 3862ab5c612a8515d2dbfed6cc23739e5727f0a2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Feb 2015 02:30:23 +0100 Subject: [PATCH 01/23] Initial release: package lang.codedom --- src/main/php/lang/codedom/AnyOf.class.php | 32 ++ .../lang/codedom/ClassDeclaration.class.php | 49 ++ src/main/php/lang/codedom/CodeUnit.class.php | 31 ++ .../codedom/ConstantDeclaration.class.php | 17 + src/main/php/lang/codedom/EitherOf.class.php | 29 ++ src/main/php/lang/codedom/Expr.class.php | 35 ++ .../lang/codedom/FieldDeclaration.class.php | 42 ++ .../codedom/InterfaceDeclaration.class.php | 46 ++ src/main/php/lang/codedom/ListOf.class.php | 29 ++ src/main/php/lang/codedom/Match.class.php | 17 + .../lang/codedom/MethodDeclaration.class.php | 48 ++ src/main/php/lang/codedom/OneOf.class.php | 28 ++ src/main/php/lang/codedom/Optional.class.php | 23 + src/main/php/lang/codedom/PhpSyntax.class.php | 158 +++++++ src/main/php/lang/codedom/Repeat.class.php | 23 + src/main/php/lang/codedom/Repeated.class.php | 24 + src/main/php/lang/codedom/Returns.class.php | 17 + src/main/php/lang/codedom/Rule.class.php | 23 + src/main/php/lang/codedom/Sequence.class.php | 31 ++ src/main/php/lang/codedom/SkipOver.class.php | 35 ++ src/main/php/lang/codedom/Stream.class.php | 98 ++++ src/main/php/lang/codedom/Token.class.php | 31 ++ src/main/php/lang/codedom/Tokens.class.php | 24 + .../lang/codedom/TraitDeclaration.class.php | 43 ++ .../php/lang/codedom/TraitUsage.class.php | 15 + .../php/lang/codedom/Unexpected.class.php | 15 + src/main/php/lang/codedom/Unmatched.class.php | 16 + src/main/php/lang/codedom/Values.class.php | 13 + .../unittest/core/PhpSyntaxTest.class.php | 441 ++++++++++++++++++ 29 files changed, 1433 insertions(+) create mode 100755 src/main/php/lang/codedom/AnyOf.class.php create mode 100755 src/main/php/lang/codedom/ClassDeclaration.class.php create mode 100755 src/main/php/lang/codedom/CodeUnit.class.php create mode 100755 src/main/php/lang/codedom/ConstantDeclaration.class.php create mode 100755 src/main/php/lang/codedom/EitherOf.class.php create mode 100755 src/main/php/lang/codedom/Expr.class.php create mode 100755 src/main/php/lang/codedom/FieldDeclaration.class.php create mode 100755 src/main/php/lang/codedom/InterfaceDeclaration.class.php create mode 100755 src/main/php/lang/codedom/ListOf.class.php create mode 100755 src/main/php/lang/codedom/Match.class.php create mode 100755 src/main/php/lang/codedom/MethodDeclaration.class.php create mode 100755 src/main/php/lang/codedom/OneOf.class.php create mode 100755 src/main/php/lang/codedom/Optional.class.php create mode 100755 src/main/php/lang/codedom/PhpSyntax.class.php create mode 100755 src/main/php/lang/codedom/Repeat.class.php create mode 100755 src/main/php/lang/codedom/Repeated.class.php create mode 100755 src/main/php/lang/codedom/Returns.class.php create mode 100755 src/main/php/lang/codedom/Rule.class.php create mode 100755 src/main/php/lang/codedom/Sequence.class.php create mode 100755 src/main/php/lang/codedom/SkipOver.class.php create mode 100755 src/main/php/lang/codedom/Stream.class.php create mode 100755 src/main/php/lang/codedom/Token.class.php create mode 100755 src/main/php/lang/codedom/Tokens.class.php create mode 100755 src/main/php/lang/codedom/TraitDeclaration.class.php create mode 100755 src/main/php/lang/codedom/TraitUsage.class.php create mode 100755 src/main/php/lang/codedom/Unexpected.class.php create mode 100755 src/main/php/lang/codedom/Unmatched.class.php create mode 100755 src/main/php/lang/codedom/Values.class.php create mode 100755 src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php diff --git a/src/main/php/lang/codedom/AnyOf.class.php b/src/main/php/lang/codedom/AnyOf.class.php new file mode 100755 index 0000000000..aeeb50782b --- /dev/null +++ b/src/main/php/lang/codedom/AnyOf.class.php @@ -0,0 +1,32 @@ +cases= $cases; + } + + public function consume($rules, $stream, $values) { + $begin= $stream->position(); + + $match= true; + do { + $case= $stream->token(); + if (isset($this->cases[$case[0]])) { + $stream->next(); + $result= $this->cases[$case[0]]->consume($rules, $stream, [is_array($case) ? $case[1] : $case]); + if ($result->matched()) { + $values[]= $result->backing(); + } else { + $stream->reset($begin); + return new Unexpected('Matched beginning but not rest: '.$result->error(), $stream->line()); + } + } else { + $match= false; + } + } while ($match); + + return new Values($values); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/ClassDeclaration.class.php b/src/main/php/lang/codedom/ClassDeclaration.class.php new file mode 100755 index 0000000000..911e6f6db0 --- /dev/null +++ b/src/main/php/lang/codedom/ClassDeclaration.class.php @@ -0,0 +1,49 @@ +modifiers= $modifiers; + $this->annotations= $annotations; + $this->name= $name; + $this->extends= $extends; + $this->implements= $implements; + $this->body= $body; + } + + public function access($modifiers) { + $this->modifiers= $modifiers; + } + + public function annotate($annotations) { + $this->annotations= $annotations; + } + + public function toString() { + return sprintf( + "%s@(%s%s %s%s%s){\n%s}", + $this->getClassName(), + $this->annotations ? $this->annotations.' ' : '', + implode(' ', Modifiers::namesOf($this->modifiers)), + $this->name, + $this->extends ? ' extends '.$this->extends : '', + $this->implements ? ' implements '.implode(', ', $this->implements) : '', + implode('', array_map(function($decl) { return ' '.str_replace("\n", "\n ", $decl->toString())."\n"; }, $this->body)) + ); + } + + public function equals($cmp) { + return $cmp instanceof self && ( + $this->modifiers === $cmp->modifiers && + $this->annotations === $cmp->annotations && + $this->name === $cmp->name && + $this->extends === $cmp->extends && + Objects::equal($this->implements, $cmp->implements) && + Objects::equal($this->body, $cmp->body) + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/CodeUnit.class.php b/src/main/php/lang/codedom/CodeUnit.class.php new file mode 100755 index 0000000000..e951eb4eef --- /dev/null +++ b/src/main/php/lang/codedom/CodeUnit.class.php @@ -0,0 +1,31 @@ +package= $package; + $this->imports= $imports; + $this->declaration= $declaration; + } + + public function toString() { + return sprintf( + "%s@(%s){\n%s %s\n}", + $this->getClassName(), + $this->package ? 'package '.$this->package : 'main', + $this->imports ? implode('', array_map(function($i) { return " use $i\n"; }, $this->imports)) : '', + str_replace("\n", "\n ", $this->declaration->toString()) + ); + } + + public function equals($cmp) { + return $cmp instanceof self && ( + $this->package === $cmp->package && + Objects::equal($this->imports, $cmp->imports) && + Objects::equal($this->declaration, $cmp->declaration) + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/ConstantDeclaration.class.php b/src/main/php/lang/codedom/ConstantDeclaration.class.php new file mode 100755 index 0000000000..e79d39db28 --- /dev/null +++ b/src/main/php/lang/codedom/ConstantDeclaration.class.php @@ -0,0 +1,17 @@ +name= $name; + $this->initial= $initial; + } + + public function equals($cmp) { + return $cmp instanceof self && ( + $this->name === $cmp->name && + $this->initial === $cmp->initial + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/EitherOf.class.php b/src/main/php/lang/codedom/EitherOf.class.php new file mode 100755 index 0000000000..1702cfce2a --- /dev/null +++ b/src/main/php/lang/codedom/EitherOf.class.php @@ -0,0 +1,29 @@ +cases= $cases; + } + + public function consume($rules, $stream, $values) { + $begin= $stream->position(); + foreach ($this->cases as $case) { + $result= $case->consume($rules, $stream, []); + if ($result->matched()) return $result; + $stream->reset($begin); + } + + $token= $stream->token(); + return new Unexpected( + sprintf( + 'Unexpected %s, expecting one of %s', + is_array($token) ? token_name($token[0]) : $token, + \xp::stringOf($this->cases) + ), + $stream->line() + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Expr.class.php b/src/main/php/lang/codedom/Expr.class.php new file mode 100755 index 0000000000..a1c599cca7 --- /dev/null +++ b/src/main/php/lang/codedom/Expr.class.php @@ -0,0 +1,35 @@ +token(true); + if ('[' === $t) { + $braces++; + $expr.= '['; + } else if ('(' === $t) { + $array++; + $expr.= '('; + } else if (']' === $t) { + $braces--; + $expr.= ']'; + } else if (')' === $t) { + $array--; + $expr.= ')'; + } else if ((',' === $t || ';' === $t) && (0 === $braces && 0 === $array)) { + return new Values(trim($expr)); + } else { + $expr.= is_array($t) ? $t[1] : $t; + } + } while ($stream->next()); + + return new Unexpected('End of file', $stream->line()); + } + + public function toString() { return $this->getClassName(); } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/FieldDeclaration.class.php b/src/main/php/lang/codedom/FieldDeclaration.class.php new file mode 100755 index 0000000000..157228f2fc --- /dev/null +++ b/src/main/php/lang/codedom/FieldDeclaration.class.php @@ -0,0 +1,42 @@ +modifiers= $modifiers; + $this->annotations= $annotations; + $this->name= $name; + $this->initial= $initial; + } + + public function access($modifiers) { + $this->modifiers= $modifiers; + } + + public function annotate($annotations) { + $this->annotations= $annotations; + } + + public function toString() { + return sprintf( + '%s@<%s $%s%s>', + $this->getClassName(), + $this->annotations ? $this->annotations.' ' : '', + implode(' ', Modifiers::namesOf($this->modifiers)), + $this->name, + $this->initial ? ' = '.$this->initial.';' : '' + ); + } + + public function equals($cmp) { + return $cmp instanceof self && ( + $this->modifiers === $cmp->modifiers && + $this->annotations === $cmp->annotations && + $this->name === $cmp->name && + $this->initial === $cmp->initial + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/InterfaceDeclaration.class.php b/src/main/php/lang/codedom/InterfaceDeclaration.class.php new file mode 100755 index 0000000000..442ff64d83 --- /dev/null +++ b/src/main/php/lang/codedom/InterfaceDeclaration.class.php @@ -0,0 +1,46 @@ +modifiers= $modifiers; + $this->annotations= $annotations; + $this->name= $name; + $this->extends= $extends; + $this->body= $body; + } + + public function access($modifiers) { + $this->modifiers= $modifiers; + } + + public function annotate($annotations) { + $this->annotations= $annotations; + } + + public function toString() { + return sprintf( + "%s@(%s%s %s%s){\n%s}", + $this->getClassName(), + $this->annotations ? $this->annotations.' ' : '', + implode(' ', Modifiers::namesOf($this->modifiers)), + $this->name, + $this->extends ? ' extends '.$this->extends : '', + implode('', array_map(function($decl) { return ' '.str_replace("\n", "\n ", $decl->toString())."\n"; }, $this->body)) + ); + } + + public function equals($cmp) { + return $cmp instanceof self && ( + $this->modifiers === $cmp->modifiers && + $this->annotations === $cmp->annotations && + $this->name === $cmp->name && + Objects::equal($this->extends, $cmp->extends) && + Objects::equal($this->body, $cmp->body) + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/ListOf.class.php b/src/main/php/lang/codedom/ListOf.class.php new file mode 100755 index 0000000000..88a9284072 --- /dev/null +++ b/src/main/php/lang/codedom/ListOf.class.php @@ -0,0 +1,29 @@ +rule= $rule; + } + + public function consume($rules, $stream, $values) { + $continue= true; + do { + $result= $this->rule->consume($rules, $stream, []); + if ($result->matched()) { + $values[]= $result->backing(); + if (',' === $stream->token()) { + $stream->next(); + } else { + $continue= false; + } + } else { + return $result; + } + } while ($continue); + + return new Values($values); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Match.class.php b/src/main/php/lang/codedom/Match.class.php new file mode 100755 index 0000000000..c892883361 --- /dev/null +++ b/src/main/php/lang/codedom/Match.class.php @@ -0,0 +1,17 @@ +consume($rules, $tokens, []); + if ($values->matched()) { + return $values->backing(); + } else { + throw new FormatException($values->error()); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/MethodDeclaration.class.php b/src/main/php/lang/codedom/MethodDeclaration.class.php new file mode 100755 index 0000000000..2aca9dd56b --- /dev/null +++ b/src/main/php/lang/codedom/MethodDeclaration.class.php @@ -0,0 +1,48 @@ +modifiers= $modifiers; + $this->annotations= $annotations; + $this->name= $name; + $this->arguments= $arguments; + $this->returns= $returns; + $this->body= $body; + } + + public function access($modifiers) { + $this->modifiers= $modifiers; + } + + public function annotate($annotations) { + $this->annotations= $annotations; + } + + public function toString() { + return sprintf( + '%s@<%s%s %s(%s)>%s', + $this->getClassName(), + $this->annotations ? $this->annotations.' ' : '', + implode(' ', Modifiers::namesOf($this->modifiers)), + $this->name, + $this->arguments, + $this->body ? ' {'.strlen($this->body).' bytes}' : '' + ); + } + + public function equals($cmp) { + return $cmp instanceof self && ( + $this->modifiers === $cmp->modifiers && + $this->annotations === $cmp->annotations && + $this->name === $cmp->name && + $this->returns === $cmp->returns && + Objects::equal($this->arguments, $cmp->arguments) && + Objects::equal($this->body, $cmp->body) + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/OneOf.class.php b/src/main/php/lang/codedom/OneOf.class.php new file mode 100755 index 0000000000..40c3f8c13a --- /dev/null +++ b/src/main/php/lang/codedom/OneOf.class.php @@ -0,0 +1,28 @@ +cases= $cases; + } + + public function consume($rules, $stream, $values) { + $case= $stream->token(); + if (isset($this->cases[$case[0]])) { + $stream->next(); + return $this->cases[$case[0]]->consume($rules, $stream, [is_array($case) ? $case[1] : $case]); + } + + return new Unexpected( + sprintf( + 'Unexpected %s, expecting one of %s', + is_array($case) ? token_name($case[0]) : $case, + implode(', ', array_map('token_name', array_keys($this->cases))) + ), + $stream->line() + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Optional.class.php b/src/main/php/lang/codedom/Optional.class.php new file mode 100755 index 0000000000..ce2d658ef8 --- /dev/null +++ b/src/main/php/lang/codedom/Optional.class.php @@ -0,0 +1,23 @@ +rule= $rule; + } + + public function consume($rules, $stream, $values) { + $begin= $stream->position(); + + $result= $this->rule->consume($rules, $stream, $values); + if ($result->matched()) { + return $result; + } else { + $stream->reset($begin); + return new Values(null); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/PhpSyntax.class.php b/src/main/php/lang/codedom/PhpSyntax.class.php new file mode 100755 index 0000000000..58c1d87f28 --- /dev/null +++ b/src/main/php/lang/codedom/PhpSyntax.class.php @@ -0,0 +1,158 @@ + new Sequence([new Token(T_OPEN_TAG), new Rule(':namespace'), new Rule(':imports'), new Rule(':uses_opt'), new Rule(':declaration')], function($values) { + $imports= $values[2]; + foreach ($values[3] as $uses) { + $imports= array_merge($imports, (array)$uses); + } + return new CodeUnit($values[1], $imports, $values[4]); + }), + ':namespace' => new Optional(new OneOf([ + T_VARIABLE => new Sequence([new Token('='), new Token(T_CONSTANT_ENCAPSED_STRING), new Token(';')], function($values) { + return strtr(substr($values[2], 1, -1), '.', '\\'); + }), + T_NAMESPACE => new Sequence([$type, new Token(';')], function($values) { + return implode('', $values[1]); + }) + ])), + ':imports' => new AnyOf([ + T_USE => new Sequence([$type, new Token(';')], function($values) { return implode('', $values[1]); }), + T_NEW => new Sequence([new Token(T_STRING), new Token('('), new Token(T_CONSTANT_ENCAPSED_STRING), new Token(')'), new Token(';')], function($values) { + return strtr(trim($values[3], '\'"'), '.', '\\'); + }) + ]), + ':uses_opt' => new AnyOf([ + T_STRING => new Sequence([new Token('('), new SkipOver('(', ')'), new Token(';')], function($values) { + if ('uses' === $values[0]) { + return array_map(function($class) { return strtr(trim($class, "'\" "), '.', '\\'); }, explode(',', $values[2])); + } else { + return null; + } + }) + ]), + ':declaration' => new Sequence( + [ + new Rule(':annotations'), + $modifiers, + new OneOf([ + T_CLASS => new Sequence([new Token(T_STRING), new Rule(':class_parent'), new Rule(':class_implements'), new Rule(':type_body')], function($values) { + return new ClassDeclaration(0, null, $values[1], $values[2], (array)$values[3], $values[4]); + }), + T_INTERFACE => new Sequence([new Token(T_STRING), new Rule(':interface_parents'), new Rule(':type_body')], function($values) { + return new InterfaceDeclaration(0, null, $values[1], (array)$values[2], $values[3]); + }), + T_TRAIT => new Sequence([new Token(T_STRING), new Rule(':type_body')], function($values) { + return new TraitDeclaration(0, null, $values[1], $values[2]); + }) + ]) + ], + function($values) { + $values[2]->annotate($values[0]); + $values[2]->access(self::modifiers($values[1])); + return $values[2]; + } + ), + ':annotations' => new Optional(new Sequence([new Token(600)], function($values) { + return $values[0]; + })), + ':class_parent' => new Optional( + new Sequence([new Token(T_EXTENDS), $type], function($values) { return implode('', $values[1]); }) + ), + ':class_implements' => new Optional( + new Sequence([new Token(T_IMPLEMENTS), new ListOf($type)], function($values) { + return array_map(function($v) { return implode('', $v); }, $values[1]); + }) + ), + ':interface_parents' => new Optional( + new Sequence([new Token(T_EXTENDS), new ListOf($type)], function($values) { + return array_map(function($v) { return implode('', $v); }, $values[1]); + }) + ), + ':type_body' => new Sequence([new Token('{'), new Repeated(new Rule(':member')), new Token('}')], function($values) { + $declared= []; + foreach ($values[1] as $decl) { + foreach ($decl as $member) { + $declared[]= $member; + } + } + return $declared; + }), + ':member' => new EitherOf([ + new Sequence([new Token(T_USE), $type, new Token(';')], function($values) { + return [new TraitUsage(implode('', $values[1]))]; + }), + new Sequence([new Token(T_CONST), new ListOf(new Rule(':const')), new Token(';')], function($values) { + return $values[1]; + }), + new Sequence( + [ + new Rule(':annotations'), + $modifiers, + new Rule(':annotations'), // Old way of annotating fields, in combination with grouped syntax + new OneOf([ + T_FUNCTION => new Sequence([new Token(T_STRING), new Token('('), new SkipOver('(', ')'), new Rule(':method')], function($values) { + return new MethodDeclaration(0, null, $values[1], $values[3], null, $values[4]); + }), + T_VARIABLE => new Sequence([new Rule(':field')], function($values) { + return new FieldDeclaration(0, null, substr($values[0], 1), $values[1]); + }) + ]) + ], + function($values) { + $values[0] && $values[3]->annotate($values[0]); + $values[2] && $values[3]->annotate($values[2]); + $values[3]->access(self::modifiers($values[1])); + return [$values[3]]; + } + ), + ]), + ':const' => new Sequence([new Token(T_STRING), new Token('='), new Expr()], function($values) { + return new ConstantDeclaration($values[0], $values[2]); + }), + ':field' => new Sequence( + [ + new Optional(new Sequence([new Token('='), new Expr()], function($values) { return $values[1]; })), + new OneOf([';' => new Returns(null), ',' => new Returns(null)]) + ], + function($values) { return $values[0]; } + ), + ':method' => new OneOf([ + ';' => new Returns(null), + '{' => new Sequence([new SkipOver('{', '}')], function($values) { return $values[1]; }) + ]) + ]; + } + + protected static function modifiers($names) { + static $modifiers= [ + 'public' => MODIFIER_PUBLIC, + 'private' => MODIFIER_PRIVATE, + 'protected' => MODIFIER_PROTECTED, + 'static' => MODIFIER_STATIC, + 'final' => MODIFIER_FINAL, + 'abstract' => MODIFIER_ABSTRACT + ]; + + $m= 0; + foreach ($names as $name) { + isset($modifiers[$name]) && $m |= $modifiers[$name]; + } + return $m; + } + + public function parse($input) { + return self::$parse[':start']->evaluate(self::$parse, new Stream($input)); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Repeat.class.php b/src/main/php/lang/codedom/Repeat.class.php new file mode 100755 index 0000000000..b148dc1a38 --- /dev/null +++ b/src/main/php/lang/codedom/Repeat.class.php @@ -0,0 +1,23 @@ +rule= $rule; + } + + public function evaluate($rules, $tokens, &$offset) { + $s= sizeof($tokens); + $values= []; + try { + do { + $values[]= $this->rule->evaluate($rules, $tokens, $offset); + } while ($offset < $s); + } catch (FormatException $e) { + // Ends loop + } + return $values; + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Repeated.class.php b/src/main/php/lang/codedom/Repeated.class.php new file mode 100755 index 0000000000..86d710b833 --- /dev/null +++ b/src/main/php/lang/codedom/Repeated.class.php @@ -0,0 +1,24 @@ +rule= $rule; + } + + public function consume($rules, $tokens, $values) { + $continue= true; + do { + $result= $this->rule->consume($rules, $tokens, []); + if ($result->matched()) { + $values[]= $result->backing(); + } else { + $continue= false; + } + } while ($continue); + + return new Values($values); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Returns.class.php b/src/main/php/lang/codedom/Returns.class.php new file mode 100755 index 0000000000..9b1cc9bbc1 --- /dev/null +++ b/src/main/php/lang/codedom/Returns.class.php @@ -0,0 +1,17 @@ +value= $value; + } + + public function consume($rules, $stream, $values) { + return new Values($this->value); + } + + public function toString() { + return $this->getClassName().'@'.\xp::stringOf($this->value); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Rule.class.php b/src/main/php/lang/codedom/Rule.class.php new file mode 100755 index 0000000000..1d4ca7efee --- /dev/null +++ b/src/main/php/lang/codedom/Rule.class.php @@ -0,0 +1,23 @@ +name= $name; + } + + public function consume($rules, $tokens, $values) { + if (isset($rules[$this->name])) { + return $rules[$this->name]->consume($rules, $tokens, []); + } + + throw new FormatException('Unknown rule '.$this->name); + } + + public function toString() { + return $this->getClassName().'(->'.$this->name.')'; + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Sequence.class.php b/src/main/php/lang/codedom/Sequence.class.php new file mode 100755 index 0000000000..6435bbaed8 --- /dev/null +++ b/src/main/php/lang/codedom/Sequence.class.php @@ -0,0 +1,31 @@ +rules= $rules; + $this->func= $func; + } + + public function consume($rules, $stream, $values) { + $begin= $stream->position(); + foreach ($this->rules as $rule) { + $result= $rule->consume($rules, $stream, []); + if ($result->matched()) { + $values[]= $result->backing(); + } else { + $stream->reset($begin); + return new Unmatched($rule, $result); + } + } + + $f= $this->func; + return new Values($f($values)); + } + + public function toString() { + return $this->getClassName().'@'.\xp::stringOf($this->rules); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/SkipOver.class.php b/src/main/php/lang/codedom/SkipOver.class.php new file mode 100755 index 0000000000..ae82ae960d --- /dev/null +++ b/src/main/php/lang/codedom/SkipOver.class.php @@ -0,0 +1,35 @@ +open= $open; + $this->close= $close; + } + + public function consume($rules, $stream, $values) { + $braces= 1; + $block= implode('', $values); + do { + $token= $stream->token(true); + if ($this->open === $token) { + $braces++; + $block.= $this->open; + } else if ($this->close === $token) { + $braces--; + if ($braces <= 0) { + $stream->next(); + return new Values(trim($block)); + } + $block.= $this->close; + } else { + $block.= is_array($token) ? $token[1] : $token; + } + } while ($stream->next()); + + return new Unexpected('End of file', $stream->line()); + } + + public function toString() { return $this->getClassName().'[`'.$this->open.'`...`'.$this->close.'`]'; } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Stream.class.php b/src/main/php/lang/codedom/Stream.class.php new file mode 100755 index 0000000000..1795da60c8 --- /dev/null +++ b/src/main/php/lang/codedom/Stream.class.php @@ -0,0 +1,98 @@ +stream= token_get_all($input); + $this->offset= 0; + $this->length= sizeof($this->stream); + $this->comments= []; + $this->token= null; + $this->line= 1; + } + + private function annotation() { + static $tokens= [T_COMMENT, T_WHITESPACE]; + + $annotation= ''; + do { + $annotation.= trim(substr($this->stream[$this->offset][1], 1)); + $this->offset++; + } while ($this->offset < $this->length && in_array($this->stream[$this->offset][0], $tokens)); + + $this->offset--; + return [T_ANNOTATION, $annotation]; + } + + /** + * Gets token + * + * @param bool $whitespace Whether to return whitespace (default: No) + * @return string + */ + public function token($whitespace= false) { + if (null === $this->token) { + $continue= true; + + do { + $next= $this->stream[$this->offset]; + if (T_DOC_COMMENT === $next[0]) { + $this->comments[]= $next[1]; + $this->line= $next[2]; + } else if (T_COMMENT === $next[0]) { + if ('#' === $next[1]{0}) { + $next= $this->annotation(); + $this->line= $this->stream[$this->offset][2]; + $continue= false; + } else { + $this->line= $next[2]; + $this->comments[]= $next[1]; + } + } else if (T_WHITESPACE === $next[0]) { + $this->line= $next[2]; + $continue= !$whitespace; + } else { + $continue= false; + } + } while ($continue && $this->offset++ < $this->length); + + $this->token= $next; + } + + // echo "@$this->offset TOKEN ", is_array($this->token) ? token_name($this->token[0]).'('.$this->token[0].': '.$this->token[1].')' : '`'.$this->token.'`', "\n"; + return $this->token; + } + + /** + * Forwards stream + * + * @return bool + */ + public function next() { + $this->offset++; + $this->token= null; + return $this->offset < $this->length; + } + + public function position() { + return $this->offset; + } + + public function line() { + return $this->line; + } + + public function reset($position) { + if ($position !== $this->offset) { + $this->offset= $position; + $this->token= null; + } + } + + public function comment() { + return array_pop($this->comments); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Token.class.php b/src/main/php/lang/codedom/Token.class.php new file mode 100755 index 0000000000..0560d71f8b --- /dev/null +++ b/src/main/php/lang/codedom/Token.class.php @@ -0,0 +1,31 @@ +token= $token; + } + + public function consume($rules, $stream, $values) { + $token= $stream->token(); + + if ($token[0] === $this->token) { + $stream->next(); + return new Values(is_array($token) ? $token[1] : $token); + } else { + return new Unexpected( + sprintf( + 'Unexpected %s, expecting %s', + is_array($token) ? token_name($token[0]) : $token, + is_int($this->token) ? token_name($this->token) : $this->token + ), + $stream->line() + ); + } + } + + public function toString() { + return $this->getClassName().'[`'.(is_int($this->token) ? token_name($this->token) : $this->token).'`]'; + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Tokens.class.php b/src/main/php/lang/codedom/Tokens.class.php new file mode 100755 index 0000000000..56888f2510 --- /dev/null +++ b/src/main/php/lang/codedom/Tokens.class.php @@ -0,0 +1,24 @@ +tokens= $tokens; + } + + public function consume($rules, $stream, $values) { + $consumed= []; + while (in_array($stream->token()[0], $this->tokens)) { + $consumed[]= $stream->token()[1]; + $stream->next(); + } + return new Values($consumed); + } + + private function nameOf($token) { return is_int($token) ? token_name($token) : '`'.$token.'`'; } + + public function toString() { + return $this->getClassName().'['.implode(' | ', array_map([$this, 'nameOf'], $this->tokens)).']'; + } +} diff --git a/src/main/php/lang/codedom/TraitDeclaration.class.php b/src/main/php/lang/codedom/TraitDeclaration.class.php new file mode 100755 index 0000000000..b422119e62 --- /dev/null +++ b/src/main/php/lang/codedom/TraitDeclaration.class.php @@ -0,0 +1,43 @@ +modifiers= $modifiers; + $this->annotations= $annotations; + $this->name= $name; + $this->body= $body; + } + + public function access($modifiers) { + $this->modifiers= $modifiers; + } + + public function annotate($annotations) { + $this->annotations= $annotations; + } + + public function toString() { + return sprintf( + "%s@(%s%s %s){\n%s}", + $this->getClassName(), + $this->annotations ? $this->annotations.' ' : '', + implode(' ', Modifiers::namesOf($this->modifiers)), + $this->name, + implode('', array_map(function($decl) { return ' '.str_replace("\n", "\n ", $decl->toString())."\n"; }, $this->body)) + ); + } + + public function equals($cmp) { + return $cmp instanceof self && ( + $this->modifiers === $cmp->modifiers && + $this->annotations === $cmp->annotations && + $this->name === $cmp->name && + Objects::equal($this->body, $cmp->body) + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/TraitUsage.class.php b/src/main/php/lang/codedom/TraitUsage.class.php new file mode 100755 index 0000000000..f9c376dfe9 --- /dev/null +++ b/src/main/php/lang/codedom/TraitUsage.class.php @@ -0,0 +1,15 @@ +name= $name; + } + + public function equals($cmp) { + return $cmp instanceof self && ( + $this->name === $cmp->name + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Unexpected.class.php b/src/main/php/lang/codedom/Unexpected.class.php new file mode 100755 index 0000000000..87163253a5 --- /dev/null +++ b/src/main/php/lang/codedom/Unexpected.class.php @@ -0,0 +1,15 @@ +message= $message; + $this->line= $line; + } + + public function matched() { return false; } + + public function error() { return $this->message; } + + public function toString() { return $this->getClassName().'["'.$this->message.'" at line '.$this->line.']'; } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Unmatched.class.php b/src/main/php/lang/codedom/Unmatched.class.php new file mode 100755 index 0000000000..7c5f263d8d --- /dev/null +++ b/src/main/php/lang/codedom/Unmatched.class.php @@ -0,0 +1,16 @@ +rule= $rule; + $this->cause= $cause; + } + + public function matched() { return false; } + + public function error() { return 'Umatched rule '.$this->rule->toString()."]\n Caused by ".$this->cause->toString(); } + + public function toString() { return $this->getClassName().'@'.$this->error(); } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Values.class.php b/src/main/php/lang/codedom/Values.class.php new file mode 100755 index 0000000000..27e35ab8a6 --- /dev/null +++ b/src/main/php/lang/codedom/Values.class.php @@ -0,0 +1,13 @@ +backing= $values; + } + + public function matched() { return true; } + + public function backing() { return $this->backing; } +} \ No newline at end of file diff --git a/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php b/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php new file mode 100755 index 0000000000..2503db621a --- /dev/null +++ b/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php @@ -0,0 +1,441 @@ +assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit('lang', [], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit('lang', ['util\Date'], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit('lang', ['util\Date', 'util\Objects'], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit('lang', ['xp\ArrayListExtensions'], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, ['Generic'], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, ['\lang\Generic', '\Serializable'], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], [ + new TraitUsage('Base') + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], [ + new TraitUsage('Base'), + new TraitUsage('\util\Observer') + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(MODIFIER_ABSTRACT, null, 'Test', 'Object', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(MODIFIER_FINAL, null, 'Test', 'Object', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit('test', [], new ClassDeclaration(0, '[@test]', 'Test', 'Object', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit('test', [], new ClassDeclaration(0, '[@test(key = "value",values = [1, 2, 3])]', 'Test', 'Object', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new MethodDeclaration(0, null, 'test', '', null, '') + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new MethodDeclaration(0, null, 'test', '', null, 'return true;') + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new MethodDeclaration(0, null, 'test', '$a= 1, Generic $b, $c= array(1)', null, 'return true;') + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new FieldDeclaration(MODIFIER_PUBLIC, null, 'test', null) + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new FieldDeclaration(MODIFIER_PUBLIC, null, 'test', '1') + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new FieldDeclaration(MODIFIER_PUBLIC, null, 'test', $array) + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new FieldDeclaration(MODIFIER_PUBLIC, null, 'a', null), + new FieldDeclaration(MODIFIER_PRIVATE, null, 'b', null), + new FieldDeclaration(MODIFIER_PROTECTED, null, 'c', null), + new FieldDeclaration(MODIFIER_STATIC, null, 'd', null) + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new FieldDeclaration(MODIFIER_PUBLIC, null, 'a', null), + new FieldDeclaration(0, null, 'b', 'true'), + new FieldDeclaration(0, null, 'c', null) + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new FieldDeclaration(MODIFIER_PUBLIC, '[@type("int")]', 'test', null) + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new FieldDeclaration(MODIFIER_PUBLIC, '[@type("int")]', 'a', null), + new FieldDeclaration(0, null, 'b', null) + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new ConstantDeclaration('TEST', '4') + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new ConstantDeclaration('TEST', '"Test"') + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new ConstantDeclaration('A', '"A"'), + new ConstantDeclaration('B', '"B"') + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new ConstantDeclaration('A', '"A"'), + new ConstantDeclaration('B', '"B"') + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], [ + new MethodDeclaration(MODIFIER_PUBLIC | MODIFIER_STATIC, null, 'newInstance', '', null, 'return new self();'), + new MethodDeclaration(MODIFIER_PRIVATE | MODIFIER_FINAL, null, 'create', '', null, ''), + new MethodDeclaration(MODIFIER_PROTECTED | MODIFIER_ABSTRACT, null, 'arguments', '', null, null) + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], [ + new MethodDeclaration(MODIFIER_PUBLIC, '[@test]', 'verify', '', null, ''), + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', ['\lang\Runnable'], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', ['\lang\Runnable', '\Serializable'], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], [ + new MethodDeclaration(0, null, 'test', '', null, null) + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], [ + new MethodDeclaration(0, null, 'a', '', null, null), + new MethodDeclaration(0, null, 'b', '', null, null) + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], [ + new MethodDeclaration(MODIFIER_PUBLIC, null, 'test', '', null, null) + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new TraitDeclaration(0, null, 'Test', [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new TraitDeclaration(0, null, 'Test', [ + new MethodDeclaration(MODIFIER_PUBLIC, null, 'test', '', null, '') + ])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit('test', ['lang\Object', 'util\Date'], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, ['lang\Object'], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + (new PhpSyntax())->parse('assertEquals( + new CodeUnit('net\xp_framework', [], new ClassDeclaration(0, null, 'net·xp_framework·Test', 'Object', [], [])), + (new PhpSyntax())->parse(' Date: Sun, 22 Feb 2015 13:12:12 +0100 Subject: [PATCH 02/23] Extract functionality out into base classes --- src/main/php/lang/codedom/BodyPart.class.php | 7 + .../lang/codedom/ClassDeclaration.class.php | 30 ++-- src/main/php/lang/codedom/CodeUnit.class.php | 27 ++++ .../codedom/ConstantDeclaration.class.php | 24 +++- .../php/lang/codedom/Decorations.class.php | 17 +++ .../lang/codedom/FieldDeclaration.class.php | 32 +++-- .../codedom/InterfaceDeclaration.class.php | 38 ++--- .../lang/codedom/MemberDeclaration.class.php | 33 +++++ .../lang/codedom/MethodDeclaration.class.php | 30 ++-- src/main/php/lang/codedom/PhpSyntax.class.php | 23 ++- .../lang/codedom/TraitDeclaration.class.php | 29 ++-- .../php/lang/codedom/TraitUsage.class.php | 29 +++- src/main/php/lang/codedom/TypeBody.class.php | 48 +++++++ .../lang/codedom/TypeDeclaration.class.php | 26 ++++ .../unittest/core/PhpSyntaxTest.class.php | 131 +++++++++--------- 15 files changed, 370 insertions(+), 154 deletions(-) create mode 100755 src/main/php/lang/codedom/BodyPart.class.php create mode 100755 src/main/php/lang/codedom/Decorations.class.php create mode 100755 src/main/php/lang/codedom/MemberDeclaration.class.php create mode 100755 src/main/php/lang/codedom/TypeBody.class.php create mode 100755 src/main/php/lang/codedom/TypeDeclaration.class.php diff --git a/src/main/php/lang/codedom/BodyPart.class.php b/src/main/php/lang/codedom/BodyPart.class.php new file mode 100755 index 0000000000..b2ca7257a1 --- /dev/null +++ b/src/main/php/lang/codedom/BodyPart.class.php @@ -0,0 +1,7 @@ +modifiers= $modifiers; - $this->annotations= $annotations; - $this->name= $name; + parent::__construct($modifiers, $annotations, $name, $body); $this->extends= $extends; $this->implements= $implements; - $this->body= $body; - } - - public function access($modifiers) { - $this->modifiers= $modifiers; - } - - public function annotate($annotations) { - $this->annotations= $annotations; } + /** + * Creates a string representation + * + * @return string + */ public function toString() { return sprintf( "%s@(%s%s %s%s%s){\n%s}", @@ -32,10 +26,16 @@ public function toString() { $this->name, $this->extends ? ' extends '.$this->extends : '', $this->implements ? ' implements '.implode(', ', $this->implements) : '', - implode('', array_map(function($decl) { return ' '.str_replace("\n", "\n ", $decl->toString())."\n"; }, $this->body)) + $this->body->toString(' ') ); } + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ public function equals($cmp) { return $cmp instanceof self && ( $this->modifiers === $cmp->modifiers && diff --git a/src/main/php/lang/codedom/CodeUnit.class.php b/src/main/php/lang/codedom/CodeUnit.class.php index e951eb4eef..5eac6becda 100755 --- a/src/main/php/lang/codedom/CodeUnit.class.php +++ b/src/main/php/lang/codedom/CodeUnit.class.php @@ -5,12 +5,33 @@ class CodeUnit extends \lang\Object { private $package, $imports, $declaration; + /** + * Creates a string representation + * + * @param string $package + * @param string[] $import + * @param lang.codedom.TypeDeclaration $declaration + */ public function __construct($package, $imports, $declaration) { $this->package= $package; $this->imports= $imports; $this->declaration= $declaration; } + /** @return string */ + public function package() { return $this->package; } + + /** @return string[] */ + public function imports() { return $this->imports; } + + /** @return lang.codedom.TypeDeclaration */ + public function declaration() { return $this->declaration; } + + /** + * Creates a string representation + * + * @return string + */ public function toString() { return sprintf( "%s@(%s){\n%s %s\n}", @@ -21,6 +42,12 @@ public function toString() { ); } + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ public function equals($cmp) { return $cmp instanceof self && ( $this->package === $cmp->package && diff --git a/src/main/php/lang/codedom/ConstantDeclaration.class.php b/src/main/php/lang/codedom/ConstantDeclaration.class.php index e79d39db28..d078d04c60 100755 --- a/src/main/php/lang/codedom/ConstantDeclaration.class.php +++ b/src/main/php/lang/codedom/ConstantDeclaration.class.php @@ -1,13 +1,31 @@ name= $name; + parent::__construct(0, null, $name); $this->initial= $initial; } + /** @return bool */ + public function isConstant() { return true; } + + /** + * Creates a string representation + * + * @return string + */ + public function toString() { + return sprintf('%s@<%s = %s>', $this->getClassName(), $this->name, $this->initial); + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ public function equals($cmp) { return $cmp instanceof self && ( $this->name === $cmp->name && diff --git a/src/main/php/lang/codedom/Decorations.class.php b/src/main/php/lang/codedom/Decorations.class.php new file mode 100755 index 0000000000..83e1579439 --- /dev/null +++ b/src/main/php/lang/codedom/Decorations.class.php @@ -0,0 +1,17 @@ +modifiers; } + + /** @return string */ + public function annotations() { return $this->annotations; } + + /** @param int $modifiers */ + public function access($modifiers) { $this->modifiers= $modifiers; } + + /** @param string $annotations */ + public function annotate($annotations) { $this->annotations= $annotations; } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/FieldDeclaration.class.php b/src/main/php/lang/codedom/FieldDeclaration.class.php index 157228f2fc..b5c58f001c 100755 --- a/src/main/php/lang/codedom/FieldDeclaration.class.php +++ b/src/main/php/lang/codedom/FieldDeclaration.class.php @@ -2,35 +2,39 @@ use lang\reflect\Modifiers; -class FieldDeclaration extends \lang\Object { - private $modifiers, $name, $initial; +class FieldDeclaration extends MemberDeclaration { + private $initial; public function __construct($modifiers, $annotations, $name, $initial) { - $this->modifiers= $modifiers; - $this->annotations= $annotations; - $this->name= $name; + parent::__construct($modifiers, $annotations, $name); $this->initial= $initial; } - public function access($modifiers) { - $this->modifiers= $modifiers; - } - - public function annotate($annotations) { - $this->annotations= $annotations; - } + /** @return bool */ + public function isField() { return true; } + /** + * Creates a string representation + * + * @return string + */ public function toString() { return sprintf( - '%s@<%s $%s%s>', + '%s@<%s%s $%s%s>', $this->getClassName(), $this->annotations ? $this->annotations.' ' : '', implode(' ', Modifiers::namesOf($this->modifiers)), $this->name, - $this->initial ? ' = '.$this->initial.';' : '' + $this->initial ? ' = '.$this->initial : '' ); } + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ public function equals($cmp) { return $cmp instanceof self && ( $this->modifiers === $cmp->modifiers && diff --git a/src/main/php/lang/codedom/InterfaceDeclaration.class.php b/src/main/php/lang/codedom/InterfaceDeclaration.class.php index 442ff64d83..21d09dceaa 100755 --- a/src/main/php/lang/codedom/InterfaceDeclaration.class.php +++ b/src/main/php/lang/codedom/InterfaceDeclaration.class.php @@ -3,25 +3,19 @@ use util\Objects; use lang\reflect\Modifiers; -class InterfaceDeclaration extends \lang\Object { - private $modifiers, $annotations, $name, $extends, $body; +class InterfaceDeclaration extends TypeDeclaration { + private $parents; - public function __construct($modifiers, $annotations, $name, $extends, $body) { - $this->modifiers= $modifiers; - $this->annotations= $annotations; - $this->name= $name; - $this->extends= $extends; - $this->body= $body; - } - - public function access($modifiers) { - $this->modifiers= $modifiers; - } - - public function annotate($annotations) { - $this->annotations= $annotations; + public function __construct($modifiers, $annotations, $name, $parents, $body) { + parent::__construct($modifiers, $annotations, $name, $body); + $this->parents= $parents; } + /** + * Creates a string representation + * + * @return string + */ public function toString() { return sprintf( "%s@(%s%s %s%s){\n%s}", @@ -29,17 +23,23 @@ public function toString() { $this->annotations ? $this->annotations.' ' : '', implode(' ', Modifiers::namesOf($this->modifiers)), $this->name, - $this->extends ? ' extends '.$this->extends : '', - implode('', array_map(function($decl) { return ' '.str_replace("\n", "\n ", $decl->toString())."\n"; }, $this->body)) + $this->parents ? ' extends '.$this->parents : '', + $this->body->toString(' ') ); } + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ public function equals($cmp) { return $cmp instanceof self && ( $this->modifiers === $cmp->modifiers && $this->annotations === $cmp->annotations && $this->name === $cmp->name && - Objects::equal($this->extends, $cmp->extends) && + Objects::equal($this->parents, $cmp->parents) && Objects::equal($this->body, $cmp->body) ); } diff --git a/src/main/php/lang/codedom/MemberDeclaration.class.php b/src/main/php/lang/codedom/MemberDeclaration.class.php new file mode 100755 index 0000000000..8932862bee --- /dev/null +++ b/src/main/php/lang/codedom/MemberDeclaration.class.php @@ -0,0 +1,33 @@ +modifiers= $modifiers; + $this->annotations= $annotations; + $this->name= $name; + } + + /** @return string */ + public function type() { return 'member'; } + + /** @return string */ + public function name() { return $this->name; } + + /** @return bool */ + public function isConstant() { return false; } + + /** @return bool */ + public function isField() { return false; } + + /** @return bool */ + public function isMethod() { return false; } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/MethodDeclaration.class.php b/src/main/php/lang/codedom/MethodDeclaration.class.php index 2aca9dd56b..90803bc013 100755 --- a/src/main/php/lang/codedom/MethodDeclaration.class.php +++ b/src/main/php/lang/codedom/MethodDeclaration.class.php @@ -3,26 +3,24 @@ use util\Objects; use lang\reflect\Modifiers; -class MethodDeclaration extends \lang\Object { - private $modifiers, $annotations, $name, $arguments, $returns, $body; +class MethodDeclaration extends MemberDeclaration { + private $arguments, $returns, $body; public function __construct($modifiers, $annotations, $name, $arguments, $returns, $body) { - $this->modifiers= $modifiers; - $this->annotations= $annotations; - $this->name= $name; + parent::__construct($modifiers, $annotations, $name); $this->arguments= $arguments; $this->returns= $returns; $this->body= $body; } - public function access($modifiers) { - $this->modifiers= $modifiers; - } - - public function annotate($annotations) { - $this->annotations= $annotations; - } + /** @return bool */ + public function isMethod() { return true; } + /** + * Creates a string representation + * + * @return string + */ public function toString() { return sprintf( '%s@<%s%s %s(%s)>%s', @@ -31,10 +29,16 @@ public function toString() { implode(' ', Modifiers::namesOf($this->modifiers)), $this->name, $this->arguments, - $this->body ? ' {'.strlen($this->body).' bytes}' : '' + $this->body ? ' { '.strlen($this->body).' bytes }' : '' ); } + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ public function equals($cmp) { return $cmp instanceof self && ( $this->modifiers === $cmp->modifiers && diff --git a/src/main/php/lang/codedom/PhpSyntax.class.php b/src/main/php/lang/codedom/PhpSyntax.class.php index 58c1d87f28..ac1bb86b14 100755 --- a/src/main/php/lang/codedom/PhpSyntax.class.php +++ b/src/main/php/lang/codedom/PhpSyntax.class.php @@ -1,8 +1,6 @@ new Sequence([new Token('{'), new Repeated(new Rule(':member')), new Token('}')], function($values) { - $declared= []; + $body= ['member' => [], 'trait' => []]; foreach ($values[1] as $decl) { - foreach ($decl as $member) { - $declared[]= $member; + foreach ($decl as $part) { + $body[$part->type()][]= $part; } } - return $declared; + return new TypeBody($body['member'], $body['trait']); }), ':member' => new EitherOf([ new Sequence([new Token(T_USE), $type, new Token(';')], function($values) { @@ -135,6 +133,12 @@ function($values) { return $values[0]; } ]; } + /** + * Parses modifier names into flags + * + * @param string[] $names + * @return int + */ protected static function modifiers($names) { static $modifiers= [ 'public' => MODIFIER_PUBLIC, @@ -152,6 +156,13 @@ protected static function modifiers($names) { return $m; } + /** + * Parses input + * + * @param string $input + * @return lang.codedom.CodeUnit + * @throws lang.FormatException + */ public function parse($input) { return self::$parse[':start']->evaluate(self::$parse, new Stream($input)); } diff --git a/src/main/php/lang/codedom/TraitDeclaration.class.php b/src/main/php/lang/codedom/TraitDeclaration.class.php index b422119e62..43323a735b 100755 --- a/src/main/php/lang/codedom/TraitDeclaration.class.php +++ b/src/main/php/lang/codedom/TraitDeclaration.class.php @@ -3,24 +3,17 @@ use util\Objects; use lang\reflect\Modifiers; -class TraitDeclaration extends \lang\Object { - private $modifiers, $annotations, $name, $body; +class TraitDeclaration extends TypeDeclaration { public function __construct($modifiers, $annotations, $name, $body) { - $this->modifiers= $modifiers; - $this->annotations= $annotations; - $this->name= $name; - $this->body= $body; - } - - public function access($modifiers) { - $this->modifiers= $modifiers; - } - - public function annotate($annotations) { - $this->annotations= $annotations; + parent::__construct($modifiers, $annotations, $name, $body); } + /** + * Creates a string representation + * + * @return string + */ public function toString() { return sprintf( "%s@(%s%s %s){\n%s}", @@ -28,10 +21,16 @@ public function toString() { $this->annotations ? $this->annotations.' ' : '', implode(' ', Modifiers::namesOf($this->modifiers)), $this->name, - implode('', array_map(function($decl) { return ' '.str_replace("\n", "\n ", $decl->toString())."\n"; }, $this->body)) + $this->body->toString(' ') ); } + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ public function equals($cmp) { return $cmp instanceof self && ( $this->modifiers === $cmp->modifiers && diff --git a/src/main/php/lang/codedom/TraitUsage.class.php b/src/main/php/lang/codedom/TraitUsage.class.php index f9c376dfe9..ac0be10add 100755 --- a/src/main/php/lang/codedom/TraitUsage.class.php +++ b/src/main/php/lang/codedom/TraitUsage.class.php @@ -1,15 +1,36 @@ name= $name; } + /** @return string */ + public function type() { return 'trait'; } + + /** + * Creates a string representation + * + * @return string + */ + public function toString() { + return sprintf('%s@(use %s)', $this->getClassName(), $this->name); + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ public function equals($cmp) { - return $cmp instanceof self && ( - $this->name === $cmp->name - ); + return $cmp instanceof self && ($this->name === $cmp->name); } } \ No newline at end of file diff --git a/src/main/php/lang/codedom/TypeBody.class.php b/src/main/php/lang/codedom/TypeBody.class.php new file mode 100755 index 0000000000..3b1410b136 --- /dev/null +++ b/src/main/php/lang/codedom/TypeBody.class.php @@ -0,0 +1,48 @@ +members= $members; + $this->traits= $traits; + } + + /** @return lang.codedom.TraitUsage */ + public function traits() { return $this->traits; } + + /** @return lang.codedom.MemberDeclaration */ + public function members() { return $this->members; } + + /** + * Creates a string representation + * + * @return string + */ + public function toString($indent= ' ') { + $s= ''; + $inset= "\n".$indent; + foreach ($this->traits as $part) { + $s.= $indent.str_replace("\n", $inset, $part->toString())."\n"; + } + foreach ($this->members as $part) { + $s.= $indent.str_replace("\n", $inset, $part->toString())."\n"; + } + return $s; + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && ( + Objects::equal($this->members, $cmp->members) && + Objects::equal($this->traits, $cmp->traits) + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/TypeDeclaration.class.php b/src/main/php/lang/codedom/TypeDeclaration.class.php new file mode 100755 index 0000000000..ae4c057503 --- /dev/null +++ b/src/main/php/lang/codedom/TypeDeclaration.class.php @@ -0,0 +1,26 @@ +modifiers= $modifiers; + $this->annotations= $annotations; + $this->name= $name; + $this->body= $body; + } + + /** @return string */ + public function name() { return $this->name; } + + /** @return lang.codedom.TypeBody */ + public function body() { return $this->body; } +} \ No newline at end of file diff --git a/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php b/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php index 2503db621a..08040c75ac 100755 --- a/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php +++ b/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php @@ -9,16 +9,17 @@ use lang\codedom\FieldDeclaration; use lang\codedom\ConstantDeclaration; use lang\codedom\TraitUsage; +use lang\codedom\TypeBody; /** - * + * Integration test for lang.codedom package */ class PhpSyntaxTest extends \unittest\TestCase { #[@test] public function class_in_global_scope() { $this->assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit('lang', [], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + new CodeUnit('lang', [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit('lang', ['util\Date'], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + new CodeUnit('lang', ['util\Date'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit('lang', ['util\Date', 'util\Objects'], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + new CodeUnit('lang', ['util\Date', 'util\Objects'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit('lang', ['xp\ArrayListExtensions'], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + new CodeUnit('lang', ['xp\ArrayListExtensions'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], [])), + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, ['Generic'], [])), + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, ['Generic'], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, ['\lang\Generic', '\Serializable'], [])), + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, ['\lang\Generic', '\Serializable'], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], new TypeBody([], [ new TraitUsage('Base') - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], new TypeBody([], [ new TraitUsage('Base'), new TraitUsage('\util\Observer') - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(MODIFIER_ABSTRACT, null, 'Test', 'Object', [], [])), + new CodeUnit(null, [], new ClassDeclaration(MODIFIER_ABSTRACT, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(MODIFIER_FINAL, null, 'Test', 'Object', [], [])), + new CodeUnit(null, [], new ClassDeclaration(MODIFIER_FINAL, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit('test', [], new ClassDeclaration(0, '[@test]', 'Test', 'Object', [], [])), + new CodeUnit('test', [], new ClassDeclaration(0, '[@test]', 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit('test', [], new ClassDeclaration(0, '[@test(key = "value",values = [1, 2, 3])]', 'Test', 'Object', [], [])), + new CodeUnit('test', [], new ClassDeclaration(0, '[@test(key = "value",values = [1, 2, 3])]', 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new MethodDeclaration(0, null, 'test', '', null, '') - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new MethodDeclaration(0, null, 'test', '', null, 'return true;') - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new MethodDeclaration(0, null, 'test', '$a= 1, Generic $b, $c= array(1)', null, 'return true;') - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new FieldDeclaration(MODIFIER_PUBLIC, null, 'test', null) - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new FieldDeclaration(MODIFIER_PUBLIC, null, 'test', '1') - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new FieldDeclaration(MODIFIER_PUBLIC, null, 'test', $array) - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new FieldDeclaration(MODIFIER_PUBLIC, null, 'a', null), new FieldDeclaration(MODIFIER_PRIVATE, null, 'b', null), new FieldDeclaration(MODIFIER_PROTECTED, null, 'c', null), new FieldDeclaration(MODIFIER_STATIC, null, 'd', null) - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new FieldDeclaration(MODIFIER_PUBLIC, null, 'a', null), new FieldDeclaration(0, null, 'b', 'true'), new FieldDeclaration(0, null, 'c', null) - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new FieldDeclaration(MODIFIER_PUBLIC, '[@type("int")]', 'test', null) - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new FieldDeclaration(MODIFIER_PUBLIC, '[@type("int")]', 'a', null), new FieldDeclaration(0, null, 'b', null) - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new ConstantDeclaration('TEST', '4') - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new ConstantDeclaration('TEST', '"Test"') - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new ConstantDeclaration('A', '"A"'), new ConstantDeclaration('B', '"B"') - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ new ConstantDeclaration('A', '"A"'), new ConstantDeclaration('B', '"B"') - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], new TypeBody([ new MethodDeclaration(MODIFIER_PUBLIC | MODIFIER_STATIC, null, 'newInstance', '', null, 'return new self();'), new MethodDeclaration(MODIFIER_PRIVATE | MODIFIER_FINAL, null, 'create', '', null, ''), new MethodDeclaration(MODIFIER_PROTECTED | MODIFIER_ABSTRACT, null, 'arguments', '', null, null) - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], [ + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], new TypeBody([ new MethodDeclaration(MODIFIER_PUBLIC, '[@test]', 'verify', '', null, ''), - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], [])), + new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', ['\lang\Runnable'], [])), + new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', ['\lang\Runnable'], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', ['\lang\Runnable', '\Serializable'], [])), + new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', ['\lang\Runnable', '\Serializable'], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], [ + new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], new TypeBody([ new MethodDeclaration(0, null, 'test', '', null, null) - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], [ + new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], new TypeBody([ new MethodDeclaration(0, null, 'a', '', null, null), new MethodDeclaration(0, null, 'b', '', null, null) - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], [ + new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], new TypeBody([ new MethodDeclaration(MODIFIER_PUBLIC, null, 'test', '', null, null) - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new TraitDeclaration(0, null, 'Test', [])), + new CodeUnit(null, [], new TraitDeclaration(0, null, 'Test', new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new TraitDeclaration(0, null, 'Test', [ + new CodeUnit(null, [], new TraitDeclaration(0, null, 'Test', new TypeBody([ new MethodDeclaration(MODIFIER_PUBLIC, null, 'test', '', null, '') - ])), + ]))), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit('test', ['lang\Object', 'util\Date'], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + new CodeUnit('test', ['lang\Object', 'util\Date'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, ['lang\Object'], new ClassDeclaration(0, null, 'Test', 'Object', [], [])), + new CodeUnit(null, ['lang\Object'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit('net\xp_framework', [], new ClassDeclaration(0, null, 'net·xp_framework·Test', 'Object', [], [])), + new CodeUnit('net\xp_framework', [], new ClassDeclaration(0, null, 'net·xp_framework·Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse(' Date: Sun, 22 Feb 2015 14:39:11 +0100 Subject: [PATCH 03/23] Verify apidoc comments access works via reflection --- src/test/config/unittest/core.ini | 3 ++ .../reflection/CommentsTest.class.php | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100755 src/test/php/net/xp_framework/unittest/reflection/CommentsTest.class.php diff --git a/src/test/config/unittest/core.ini b/src/test/config/unittest/core.ini index 72f07a3b25..1a4f7d4c6f 100644 --- a/src/test/config/unittest/core.ini +++ b/src/test/config/unittest/core.ini @@ -90,6 +90,9 @@ class="net.xp_framework.unittest.reflection.XPClassTest" [class-casting] class="net.xp_framework.unittest.reflection.ClassCastingTest" +[comments] +class="net.xp_framework.unittest.reflection.CommentsTest" + [destructor] class="net.xp_framework.unittest.core.DestructorTest" diff --git a/src/test/php/net/xp_framework/unittest/reflection/CommentsTest.class.php b/src/test/php/net/xp_framework/unittest/reflection/CommentsTest.class.php new file mode 100755 index 0000000000..045aa59025 --- /dev/null +++ b/src/test/php/net/xp_framework/unittest/reflection/CommentsTest.class.php @@ -0,0 +1,42 @@ +fixture= XPClass::forName('net.xp_framework.unittest.reflection.TestClass'); + } + + #[@test] + public function for_class_without_apidoc() { + $this->assertNull($this->getClass()->getComment()); + } + + #[@test] + public function for_class() { + $this->assertEquals('Test class', $this->fixture->getComment()); + } + + #[@test] + public function for_method() { + $this->assertEquals('Retrieve date', $this->fixture->getMethod('getDate')->getComment()); + } + + #[@test] + public function for_method_without_apidoc() { + $this->assertNull($this->fixture->getMethod('notDocumented')->getComment()); + } + + #[@test] + public function for_constructor() { + $this->assertEquals('Constructor', $this->fixture->getConstructor()->getComment()); + } + +} From 63b3d57f878a279832ee28931f26c3bdfc6cbaac Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Feb 2015 14:40:05 +0100 Subject: [PATCH 04/23] Add thrown exceptions, distinguish between constructor and methods --- src/main/php/lang/codedom/BodyPart.class.php | 4 + .../codedom/ConstantDeclaration.class.php | 6 ++ .../codedom/ConstructorDeclaration.class.php | 71 ++++++++++++++++++ .../lang/codedom/MethodDeclaration.class.php | 32 +++++++- src/main/php/lang/codedom/PhpSyntax.class.php | 75 ++++++++++++++++++- src/main/php/lang/codedom/Sequence.class.php | 7 +- src/main/php/lang/codedom/Stream.class.php | 42 +++++++---- .../unittest/core/PhpSyntaxTest.class.php | 54 ++++++++++--- 8 files changed, 255 insertions(+), 36 deletions(-) create mode 100755 src/main/php/lang/codedom/ConstructorDeclaration.class.php diff --git a/src/main/php/lang/codedom/BodyPart.class.php b/src/main/php/lang/codedom/BodyPart.class.php index b2ca7257a1..53a6a09a33 100755 --- a/src/main/php/lang/codedom/BodyPart.class.php +++ b/src/main/php/lang/codedom/BodyPart.class.php @@ -1,5 +1,9 @@ initial= $initial; diff --git a/src/main/php/lang/codedom/ConstructorDeclaration.class.php b/src/main/php/lang/codedom/ConstructorDeclaration.class.php new file mode 100755 index 0000000000..eaf0e40eab --- /dev/null +++ b/src/main/php/lang/codedom/ConstructorDeclaration.class.php @@ -0,0 +1,71 @@ +arguments= $arguments; + $this->throws= $throws; + $this->body= $body; + } + + /** @return bool */ + public function isMethod() { return true; } + + /** @return string[] */ + public function arguments() { return $this->arguments; } + + /** @return string */ + public function returns() { return null; } + + /** @return string[] */ + public function throws() { return $this->throws; } + + /** + * Creates a string representation + * + * @return string + */ + public function toString() { + return sprintf( + '%s@<%s%s %s(%s)>%s%s', + $this->getClassName(), + $this->annotations ? $this->annotations.' ' : '', + implode(' ', Modifiers::namesOf($this->modifiers)), + $this->name, + implode(', ', $this->arguments), + $this->throws ? ' throws '.implode(' ', $this->throws) : '', + $this->body ? ' { '.strlen($this->body).' bytes }' : '' + ); + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && ( + $this->modifiers === $cmp->modifiers && + $this->annotations === $cmp->annotations && + $this->name === $cmp->name && + Objects::equal($this->arguments, $cmp->arguments) && + Objects::equal($this->throws, $cmp->throws) && + Objects::equal($this->body, $cmp->body) + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/MethodDeclaration.class.php b/src/main/php/lang/codedom/MethodDeclaration.class.php index 90803bc013..66fad12b67 100755 --- a/src/main/php/lang/codedom/MethodDeclaration.class.php +++ b/src/main/php/lang/codedom/MethodDeclaration.class.php @@ -4,18 +4,39 @@ use lang\reflect\Modifiers; class MethodDeclaration extends MemberDeclaration { - private $arguments, $returns, $body; + private $arguments, $returns, $throws, $body; - public function __construct($modifiers, $annotations, $name, $arguments, $returns, $body) { + /** + * Creates a new method declaration + * + * @param int $modifiers + * @param string $annotations + * @param string $name + * @param string[] $arguments Argument types + * @param string $returns Return type + * @param string[] $throws Exception types + * @param string $body Code in body as string + */ + public function __construct($modifiers, $annotations, $name, $arguments, $returns, $throws, $body) { parent::__construct($modifiers, $annotations, $name); $this->arguments= $arguments; $this->returns= $returns; + $this->throws= $throws; $this->body= $body; } /** @return bool */ public function isMethod() { return true; } + /** @return string[] */ + public function arguments() { return $this->arguments; } + + /** @return string */ + public function returns() { return $this->returns; } + + /** @return string[] */ + public function throws() { return $this->throws; } + /** * Creates a string representation * @@ -23,12 +44,14 @@ public function isMethod() { return true; } */ public function toString() { return sprintf( - '%s@<%s%s %s(%s)>%s', + '%s@<%s%s %s(%s): %s>%s%s', $this->getClassName(), $this->annotations ? $this->annotations.' ' : '', implode(' ', Modifiers::namesOf($this->modifiers)), $this->name, - $this->arguments, + implode(', ', $this->arguments), + $this->returns, + $this->throws ? ' throws '.implode(' ', $this->throws) : '', $this->body ? ' { '.strlen($this->body).' bytes }' : '' ); } @@ -46,6 +69,7 @@ public function equals($cmp) { $this->name === $cmp->name && $this->returns === $cmp->returns && Objects::equal($this->arguments, $cmp->arguments) && + Objects::equal($this->throws, $cmp->throws) && Objects::equal($this->body, $cmp->body) ); } diff --git a/src/main/php/lang/codedom/PhpSyntax.class.php b/src/main/php/lang/codedom/PhpSyntax.class.php index ac1bb86b14..382928bcb3 100755 --- a/src/main/php/lang/codedom/PhpSyntax.class.php +++ b/src/main/php/lang/codedom/PhpSyntax.class.php @@ -100,8 +100,13 @@ function($values) { $modifiers, new Rule(':annotations'), // Old way of annotating fields, in combination with grouped syntax new OneOf([ - T_FUNCTION => new Sequence([new Token(T_STRING), new Token('('), new SkipOver('(', ')'), new Rule(':method')], function($values) { - return new MethodDeclaration(0, null, $values[1], $values[3], null, $values[4]); + T_FUNCTION => new Sequence([new Token(T_STRING), new Token('('), new SkipOver('(', ')'), new Rule(':method')], function($values, $stream) { + $details= self::details($stream->comment()); + if ('__construct' === $values[1]) { + return new ConstructorDeclaration(0, null, $values[1], $details['args'], $details['throws'], $values[4]); + } else { + return new MethodDeclaration(0, null, $values[1], $details['args'], $details['returns'], $details['throws'], $values[4]); + } }), T_VARIABLE => new Sequence([new Rule(':field')], function($values) { return new FieldDeclaration(0, null, substr($values[0], 1), $values[1]); @@ -139,7 +144,7 @@ function($values) { return $values[0]; } * @param string[] $names * @return int */ - protected static function modifiers($names) { + private static function modifiers($names) { static $modifiers= [ 'public' => MODIFIER_PUBLIC, 'private' => MODIFIER_PRIVATE, @@ -156,6 +161,70 @@ protected static function modifiers($names) { return $m; } + /** + * Returns position of matching closing brace, or the string's length + * if no closing / opening brace is found. + * + * @param string $text + * @param string $open + * @param string $close + * @param int + */ + private static function matching($text, $open, $close) { + for ($braces= $open.$close, $i= 0, $b= 0, $s= strlen($text); $i < $s; $i+= strcspn($text, $braces, $i)) { + if ($text{$i} === $open) { + $b++; + } else if ($text{$i} === $close) { + if (0 === --$b) return $i + 1; + } + $i++; + } + return $i; + } + + /** + * Extracts type from a text + * + * @param string $text + * @return string + */ + private static function typeIn($text) { + if (0 === strncmp($text, 'function(', 9)) { + $p= self::matching($text, '(', ')'); + $p+= strspn($text, ': ', $p); + return substr($text, 0, $p).self::typeIn(substr($text, $p)); + } else if (strstr($text, '<')) { + $p= self::matching($text, '<', '>'); + return substr($text, 0, $p); + } else { + return substr($text, 0, strcspn($text, ' ')); + } + } + + /** + * Parses methods' api doc comments + * + * @param string $comment + * @return [:var] + */ + private static function details($comment) { + $matches= null; + preg_match_all('/@([a-z]+)\s*([^\r\n]+)?/', $comment, $matches, PREG_SET_ORDER); + + $arg= 0; + $details= ['args' => [], 'returns' => 'var', 'throws' => []]; + foreach ($matches as $match) { + if ('param' === $match[1]) { + $details['args'][$arg++]= self::typeIn($match[2]); + } else if ('return' === $match[1]) { + $details['returns']= self::typeIn($match[2]); + } else if ('throws' === $match[1]) { + $details['throws'][]= self::typeIn($match[2]); + } + } + return $details; + } + /** * Parses input * diff --git a/src/main/php/lang/codedom/Sequence.class.php b/src/main/php/lang/codedom/Sequence.class.php index 6435bbaed8..9a76594055 100755 --- a/src/main/php/lang/codedom/Sequence.class.php +++ b/src/main/php/lang/codedom/Sequence.class.php @@ -22,9 +22,14 @@ public function consume($rules, $stream, $values) { } $f= $this->func; - return new Values($f($values)); + return new Values($f($values, $stream)); } + /** + * Creates a string representation + * + * @return string + */ public function toString() { return $this->getClassName().'@'.\xp::stringOf($this->rules); } diff --git a/src/main/php/lang/codedom/Stream.class.php b/src/main/php/lang/codedom/Stream.class.php index 1795da60c8..2844363b73 100755 --- a/src/main/php/lang/codedom/Stream.class.php +++ b/src/main/php/lang/codedom/Stream.class.php @@ -3,13 +3,13 @@ define('T_ANNOTATION', 600); class Stream extends \lang\Object { - private $stream, $offset, $length, $comments, $token, $line; + private $stream, $offset, $length, $comment, $token, $line; public function __construct($input) { $this->stream= token_get_all($input); $this->offset= 0; $this->length= sizeof($this->stream); - $this->comments= []; + $this->comment= null; $this->token= null; $this->line= 1; } @@ -40,7 +40,7 @@ public function token($whitespace= false) { do { $next= $this->stream[$this->offset]; if (T_DOC_COMMENT === $next[0]) { - $this->comments[]= $next[1]; + $this->comment= $next[1]; $this->line= $next[2]; } else if (T_COMMENT === $next[0]) { if ('#' === $next[1]{0}) { @@ -49,7 +49,6 @@ public function token($whitespace= false) { $continue= false; } else { $this->line= $next[2]; - $this->comments[]= $next[1]; } } else if (T_WHITESPACE === $next[0]) { $this->line= $next[2]; @@ -66,6 +65,23 @@ public function token($whitespace= false) { return $this->token; } + /** + * Gets last comment + * + * @return string + */ + public function comment() { + $comment= $this->comment; + $this->comment= null; + return $comment; + } + + /** @return int */ + public function line() { return $this->line; } + + /** @return int */ + public function position() { return $this->offset; } + /** * Forwards stream * @@ -77,22 +93,16 @@ public function next() { return $this->offset < $this->length; } - public function position() { - return $this->offset; - } - - public function line() { - return $this->line; - } - + /** + * Resets stream position to a given offset, which e.g. was previously + * retrieved via position() + * + * @param int $position + */ public function reset($position) { if ($position !== $this->offset) { $this->offset= $position; $this->token= null; } } - - public function comment() { - return array_pop($this->comments); - } } \ No newline at end of file diff --git a/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php b/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php index 08040c75ac..a783a474bd 100755 --- a/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php +++ b/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php @@ -6,6 +6,7 @@ use lang\codedom\InterfaceDeclaration; use lang\codedom\TraitDeclaration; use lang\codedom\MethodDeclaration; +use lang\codedom\ConstructorDeclaration; use lang\codedom\FieldDeclaration; use lang\codedom\ConstantDeclaration; use lang\codedom\TraitUsage; @@ -146,17 +147,27 @@ class Test extends Object { } public function class_with_method() { $this->assertEquals( new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ - new MethodDeclaration(0, null, 'test', '', null, '') + new MethodDeclaration(0, null, 'test', [], 'var', [], '') ]))), (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ + new ConstructorDeclaration(0, null, '__construct', [], [], '') + ]))), + (new PhpSyntax())->parse('assertEquals( new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ - new MethodDeclaration(0, null, 'test', '', null, 'return true;') + new MethodDeclaration(0, null, 'test', [], 'var', [], 'return true;') ]))), (new PhpSyntax())->parse('assertEquals( new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ - new MethodDeclaration(0, null, 'test', '$a= 1, Generic $b, $c= array(1)', null, 'return true;') + new MethodDeclaration(0, null, 'test', [], 'var', [], 'return true;') ]))), (new PhpSyntax())->parse('assertEquals( + new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ + new MethodDeclaration(0, null, 'test', ['string'], 'bool', ['lang.Throwable'], 'return true;') + ]))), + (new PhpSyntax())->parse('assertEquals( @@ -302,9 +332,9 @@ public function class_with_grouped_constants() { public function class_with_methods_with_modifiers() { $this->assertEquals( new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], new TypeBody([ - new MethodDeclaration(MODIFIER_PUBLIC | MODIFIER_STATIC, null, 'newInstance', '', null, 'return new self();'), - new MethodDeclaration(MODIFIER_PRIVATE | MODIFIER_FINAL, null, 'create', '', null, ''), - new MethodDeclaration(MODIFIER_PROTECTED | MODIFIER_ABSTRACT, null, 'arguments', '', null, null) + new MethodDeclaration(MODIFIER_PUBLIC | MODIFIER_STATIC, null, 'newInstance', [], 'var', [], 'return new self();'), + new MethodDeclaration(MODIFIER_PRIVATE | MODIFIER_FINAL, null, 'create', [], 'var', [], ''), + new MethodDeclaration(MODIFIER_PROTECTED | MODIFIER_ABSTRACT, null, 'arguments', [], 'var', [], null) ]))), (new PhpSyntax())->parse('assertEquals( new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', null, [], new TypeBody([ - new MethodDeclaration(MODIFIER_PUBLIC, '[@test]', 'verify', '', null, ''), + new MethodDeclaration(MODIFIER_PUBLIC, '[@test]', 'verify', [], 'var', [], ''), ]))), (new PhpSyntax())->parse('assertEquals( new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], new TypeBody([ - new MethodDeclaration(0, null, 'test', '', null, null) + new MethodDeclaration(0, null, 'test', [], 'var', [], null) ]))), (new PhpSyntax())->parse('assertEquals( new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], new TypeBody([ - new MethodDeclaration(0, null, 'a', '', null, null), - new MethodDeclaration(0, null, 'b', '', null, null) + new MethodDeclaration(0, null, 'a', [], 'var', [], null), + new MethodDeclaration(0, null, 'b', [], 'var', [], null) ]))), (new PhpSyntax())->parse('assertEquals( new CodeUnit(null, [], new InterfaceDeclaration(0, null, 'Test', [], new TypeBody([ - new MethodDeclaration(MODIFIER_PUBLIC, null, 'test', '', null, null) + new MethodDeclaration(MODIFIER_PUBLIC, null, 'test', [], 'var', [], null) ]))), (new PhpSyntax())->parse('assertEquals( new CodeUnit(null, [], new TraitDeclaration(0, null, 'Test', new TypeBody([ - new MethodDeclaration(MODIFIER_PUBLIC, null, 'test', '', null, '') + new MethodDeclaration(MODIFIER_PUBLIC, null, 'test', [], 'var', [], '') ]))), (new PhpSyntax())->parse(' Date: Sun, 22 Feb 2015 14:44:06 +0100 Subject: [PATCH 05/23] Refactor: Use new lang.codedom API to parse code --- src/main/php/lang/XPClass.class.php | 14 +- .../php/lang/reflect/ClassParser.class.php | 146 ++++-------------- src/main/php/lang/reflect/Routine.class.php | 14 +- .../reflection/ClassDetailsTest.class.php | 72 --------- 4 files changed, 57 insertions(+), 189 deletions(-) diff --git a/src/main/php/lang/XPClass.class.php b/src/main/php/lang/XPClass.class.php index 6ba239d84c..85084a096f 100755 --- a/src/main/php/lang/XPClass.class.php +++ b/src/main/php/lang/XPClass.class.php @@ -467,7 +467,6 @@ public function getDeclaredInterfaces() { } return $r; } - /** * Retrieves the api doc comment for this class. Returns NULL if @@ -476,8 +475,17 @@ public function getDeclaredInterfaces() { * @return string */ public function getComment() { - if (!($details= self::detailsForClass($this->name))) return null; - return $details['class'][DETAIL_COMMENT]; + $comment= $this->_reflect->getDocComment(); + if (false === $comment) { + $class= $this->_reflect->getName(); + return isset(\xp::$meta[$class]['class'][DETAIL_COMMENT]) ? \xp::$meta[$class]['class'][DETAIL_COMMENT]: null; + } else { + return trim(preg_replace('/\n\s+\* ?/', "\n", "\n".substr( + $comment, + 4, // "/**\n" + strpos($comment, '* @')- 2 // position of first details token + ))); + } } /** diff --git a/src/main/php/lang/reflect/ClassParser.class.php b/src/main/php/lang/reflect/ClassParser.class.php index 83f0918637..94d458a999 100755 --- a/src/main/php/lang/reflect/ClassParser.class.php +++ b/src/main/php/lang/reflect/ClassParser.class.php @@ -213,8 +213,10 @@ public function parseAnnotations($bytes, $context, $imports= [], $line= -1) { 'multi-value' ]; - $tokens= token_get_all(' [], 1 => []]; + if (null === $bytes) return $annotations; + + $tokens= token_get_all(' [], 1 => []]; - $imports= []; - $comment= null; - $parsed= ''; - $tokens= token_get_all($bytes); - for ($i= 0, $s= sizeof($tokens); $i < $s; $i++) { - switch ($tokens[$i][0]) { - case T_USE: - if (isset($details['class'])) break; // Inside class, e.g. function() use(...) {} - $type= ''; - while (';' !== $tokens[++$i] && $i < $s) { - T_WHITESPACE === $tokens[$i][0] || $type.= $tokens[$i][1]; - } - $imports[substr($type, strrpos($type, '\\')+ 1)]= strtr($type, '\\', '.'); - break; - - case T_DOC_COMMENT: - $comment= $tokens[$i][1]; - break; - - case T_COMMENT: - if ('#' === $tokens[$i][1]{0}) { // Annotations, #[@test] - if ('[' === $tokens[$i][1]{1}) { - $parsed= substr($tokens[$i][1], 2); - } else { - $parsed.= substr($tokens[$i][1], 1); - } - } - break; + $codeunit= (new \lang\codedom\PhpSyntax())->parse(ltrim($bytes)); - case T_CLASS: - case T_INTERFACE: - if ($parsed) { - $annotations= $this->parseAnnotations($parsed, $context, $imports, isset($tokens[$i][2]) ? $tokens[$i][2] : -1); - $parsed= ''; - } - $details['class']= [ - DETAIL_COMMENT => trim(preg_replace('/\n\s+\* ?/', "\n", "\n".substr( - $comment, - 4, // "/**\n" - strpos($comment, '* @')- 2 // position of first details token - ))), - DETAIL_ANNOTATIONS => $annotations[0] - ]; - $annotations= [0 => [], 1 => []]; - $comment= null; - break; - - case T_VARIABLE: // Have a member variable - if ($parsed) { - $annotations= $this->parseAnnotations($parsed, $context, $imports, isset($tokens[$i][2]) ? $tokens[$i][2] : -1); - $parsed= ''; - } - $name= substr($tokens[$i][1], 1); - $details[0][$name]= [ - DETAIL_ANNOTATIONS => $annotations[0] - ]; - $annotations= [0 => [], 1 => []]; - break; - - case T_FUNCTION: - if ($parsed) { - $annotations= $this->parseAnnotations($parsed, $context, $imports, isset($tokens[$i][2]) ? $tokens[$i][2] : -1); - $parsed= ''; - } - $i+= 2; - $m= $tokens[$i][1]; - $details[1][$m]= [ - DETAIL_ARGUMENTS => [], - DETAIL_RETURNS => null, - DETAIL_THROWS => [], - DETAIL_COMMENT => trim(preg_replace('/\n\s+\* ?/', "\n", "\n".substr( - $comment, - 4, // "/**\n" - strpos($comment, '* @')- 2 // position of first details token - ))), - DETAIL_ANNOTATIONS => $annotations[0], - DETAIL_TARGET_ANNO => $annotations[1] - ]; - $annotations= [0 => [], 1 => []]; - $matches= null; - preg_match_all('/@([a-z]+)\s*([^\r\n]+)?/', $comment, $matches, PREG_SET_ORDER); - $comment= null; - $arg= 0; - foreach ($matches as $match) { - switch ($match[1]) { - case 'param': - $details[1][$m][DETAIL_ARGUMENTS][$arg++]= $this->typeIn($match[2]); - break; - - case 'return': - $details[1][$m][DETAIL_RETURNS]= $this->typeIn($match[2]); - break; + $imports= []; + foreach ($codeunit->imports() as $type) { + $imports[substr($type, strrpos($type, '\\')+ 1)]= strtr($type, '\\', '.'); + } - case 'throws': - $details[1][$m][DETAIL_THROWS][]= $this->typeIn($match[2]); - break; - } - } - $b= 0; - while (++$i < $s) { - if ('{' === $tokens[$i][0]) { - $b++; - } else if ('}' === $tokens[$i][0]) { - if (0 === --$b) break; - } else if (0 === $b && ';' === $tokens[$i][0]) { - break; // Abstract or interface method - } - } - break; + $decl= $codeunit->declaration(); + $annotations= $this->parseAnnotations($decl->annotations(), $context, $imports, -1); + $details= [ + 0 => [], // Fields + 1 => [], // Methods + 'class' => [ + DETAIL_COMMENT => null, + DETAIL_ANNOTATIONS => $annotations[0] + ] + ]; - default: - // Empty + foreach ($decl->body()->members() as $member) { + $annotations= $this->parseAnnotations($member->annotations(), $context, $imports, -1); + if ($member->isField()) { + $details[0][$member->name()]= [ + DETAIL_ANNOTATIONS => $annotations[0] + ]; + } else if ($member->isMethod()) { + $details[1][$member->name()]= [ + DETAIL_ARGUMENTS => $member->arguments(), + DETAIL_RETURNS => $member->returns(), + DETAIL_THROWS => $member->throws(), + DETAIL_COMMENT => null, + DETAIL_ANNOTATIONS => $annotations[0], + DETAIL_TARGET_ANNO => $annotations[1] + ]; } } + return $details; } } \ No newline at end of file diff --git a/src/main/php/lang/reflect/Routine.class.php b/src/main/php/lang/reflect/Routine.class.php index 6d0c13521b..8099697664 100755 --- a/src/main/php/lang/reflect/Routine.class.php +++ b/src/main/php/lang/reflect/Routine.class.php @@ -180,8 +180,18 @@ public function getDeclaringClass() { * @return string */ public function getComment() { - if (!($details= \lang\XPClass::detailsForMethod($this->_reflect->getDeclaringClass()->getName(), $this->_reflect->getName()))) return null; - return $details[DETAIL_COMMENT]; + $comment= $this->_reflect->getDocComment(); + if (false === $comment) { + $class= $this->_reflect->getDeclaringClass()->getName(); + $name= $this->_reflect->getName(); + return isset(\xp::$meta[$class][1][$name][DETAIL_COMMENT]) ? \xp::$meta[$class][1][$name][DETAIL_COMMENT]: null; + } else { + return trim(preg_replace('/\n\s+\* ?/', "\n", "\n".substr( + $comment, + 4, // "/**\n" + strpos($comment, '* @')- 2 // position of first details token + ))); + } } /** diff --git a/src/test/php/net/xp_framework/unittest/reflection/ClassDetailsTest.class.php b/src/test/php/net/xp_framework/unittest/reflection/ClassDetailsTest.class.php index 5e800e8718..cc292a428f 100644 --- a/src/test/php/net/xp_framework/unittest/reflection/ClassDetailsTest.class.php +++ b/src/test/php/net/xp_framework/unittest/reflection/ClassDetailsTest.class.php @@ -35,33 +35,6 @@ public function test() { } return $details[1]['test']; } - #[@test] - public function commentString() { - $details= $this->parseComment(' - /** - * A protected method - * - * Note: Not compatible with PHP 4.1.2! - * - * @param string param1 - */ - '); - $this->assertEquals( - "A protected method\n\nNote: Not compatible with PHP 4.1.2!", - $details[DETAIL_COMMENT] - ); - } - - #[@test] - public function noCommentString() { - $details= $this->parseComment(' - /** - * @see php://comment - */ - '); - $this->assertEquals('', $details[DETAIL_COMMENT]); - } - #[@test] public function scalar_parameter() { $details= $this->parseComment('/** @param string param1 */'); @@ -154,51 +127,6 @@ public function int_return_type() { $this->assertEquals('int', $details[DETAIL_RETURNS]); } - #[@test] - public function withClosure() { - $details= (new ClassParser())->parseDetails(''); - $this->assertEquals('Creates a new answer', $details[1]['newAnswer'][DETAIL_COMMENT]); - } - - #[@test] - public function withClosures() { - $details= (new ClassParser())->parseDetails(''); - $this->assertEquals('Creates a new question', $details[1]['newQuestion'][DETAIL_COMMENT]); - } - /** @return [:var] */ protected function dummyDetails() { return (new ClassParser())->parseDetails(' Date: Sun, 22 Feb 2015 14:46:19 +0100 Subject: [PATCH 06/23] Remove DETAIL_COMMENT from class details --- src/main/php/lang/reflect/ClassParser.class.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/reflect/ClassParser.class.php b/src/main/php/lang/reflect/ClassParser.class.php index 94d458a999..5cb71c3dcc 100755 --- a/src/main/php/lang/reflect/ClassParser.class.php +++ b/src/main/php/lang/reflect/ClassParser.class.php @@ -350,24 +350,18 @@ public function parseDetails($bytes, $context= '') { $details= [ 0 => [], // Fields 1 => [], // Methods - 'class' => [ - DETAIL_COMMENT => null, - DETAIL_ANNOTATIONS => $annotations[0] - ] + 'class' => [DETAIL_ANNOTATIONS => $annotations[0]] ]; foreach ($decl->body()->members() as $member) { $annotations= $this->parseAnnotations($member->annotations(), $context, $imports, -1); if ($member->isField()) { - $details[0][$member->name()]= [ - DETAIL_ANNOTATIONS => $annotations[0] - ]; + $details[0][$member->name()]= [DETAIL_ANNOTATIONS => $annotations[0]]; } else if ($member->isMethod()) { $details[1][$member->name()]= [ DETAIL_ARGUMENTS => $member->arguments(), DETAIL_RETURNS => $member->returns(), DETAIL_THROWS => $member->throws(), - DETAIL_COMMENT => null, DETAIL_ANNOTATIONS => $annotations[0], DETAIL_TARGET_ANNO => $annotations[1] ]; From 71a1cf1d190b644e2d58fe30b3601f78895fe5e9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Feb 2015 14:47:44 +0100 Subject: [PATCH 07/23] Remove methods now in lang.codedom.PhpSyntax --- .../php/lang/reflect/ClassParser.class.php | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/src/main/php/lang/reflect/ClassParser.class.php b/src/main/php/lang/reflect/ClassParser.class.php index 5cb71c3dcc..3e0e85593f 100755 --- a/src/main/php/lang/reflect/ClassParser.class.php +++ b/src/main/php/lang/reflect/ClassParser.class.php @@ -290,46 +290,6 @@ public function parseAnnotations($bytes, $context, $imports= [], $line= -1) { raise('lang.ClassFormatException', 'Parse error: Unterminated '.$states[$state].' in '.$place); } - /** - * Returns position of matching closing brace, or the string's length - * if no closing / opening brace is found. - * - * @param string $text - * @param string $open - * @param string $close - * @param int - */ - protected function matching($text, $open, $close) { - for ($braces= $open.$close, $i= 0, $b= 0, $s= strlen($text); $i < $s; $i+= strcspn($text, $braces, $i)) { - if ($text{$i} === $open) { - $b++; - } else if ($text{$i} === $close) { - if (0 === --$b) return $i + 1; - } - $i++; - } - return $i; - } - - /** - * Extracts type from a text - * - * @param string $text - * @return string - */ - protected function typeIn($text) { - if (0 === strncmp($text, 'function(', 9)) { - $p= $this->matching($text, '(', ')'); - $p+= strspn($text, ': ', $p); - return substr($text, 0, $p).$this->typeIn(substr($text, $p)); - } else if (strstr($text, '<')) { - $p= $this->matching($text, '<', '>'); - return substr($text, 0, $p); - } else { - return substr($text, 0, strcspn($text, ' ')); - } - } - /** * Parse details from a given input string * From 66904342db00997dda41e106ee318f42a2c7aae4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Feb 2015 14:54:10 +0100 Subject: [PATCH 08/23] Normalize package name to dotted form --- src/main/php/lang/codedom/PhpSyntax.class.php | 4 ++-- .../xp_framework/unittest/core/PhpSyntaxTest.class.php | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/codedom/PhpSyntax.class.php b/src/main/php/lang/codedom/PhpSyntax.class.php index 382928bcb3..2f94e7d618 100755 --- a/src/main/php/lang/codedom/PhpSyntax.class.php +++ b/src/main/php/lang/codedom/PhpSyntax.class.php @@ -19,10 +19,10 @@ static function __static() { }), ':namespace' => new Optional(new OneOf([ T_VARIABLE => new Sequence([new Token('='), new Token(T_CONSTANT_ENCAPSED_STRING), new Token(';')], function($values) { - return strtr(substr($values[2], 1, -1), '.', '\\'); + return substr($values[2], 1, -1); }), T_NAMESPACE => new Sequence([$type, new Token(';')], function($values) { - return implode('', $values[1]); + return strtr(implode('', $values[1]), '\\', '.'); }) ])), ':imports' => new AnyOf([ diff --git a/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php b/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php index a783a474bd..ccf9745fda 100755 --- a/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php +++ b/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php @@ -33,6 +33,14 @@ public function class_in_namespace() { ); } + #[@test] + public function class_in_sub_namespace() { + $this->assertEquals( + new CodeUnit('lang.codedom', [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), + (new PhpSyntax())->parse('assertEquals( @@ -465,7 +473,7 @@ public function legacy_uses() { #[@test] public function legacy_package() { $this->assertEquals( - new CodeUnit('net\xp_framework', [], new ClassDeclaration(0, null, 'net·xp_framework·Test', 'Object', [], new TypeBody())), + new CodeUnit('net.xp_framework', [], new ClassDeclaration(0, null, 'net·xp_framework·Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse(' Date: Sun, 22 Feb 2015 14:56:11 +0100 Subject: [PATCH 09/23] Normalize import names to dotted form --- src/main/php/lang/codedom/PhpSyntax.class.php | 8 +++++--- .../xp_framework/unittest/core/PhpSyntaxTest.class.php | 10 +++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/codedom/PhpSyntax.class.php b/src/main/php/lang/codedom/PhpSyntax.class.php index 2f94e7d618..c7fabe1234 100755 --- a/src/main/php/lang/codedom/PhpSyntax.class.php +++ b/src/main/php/lang/codedom/PhpSyntax.class.php @@ -26,15 +26,17 @@ static function __static() { }) ])), ':imports' => new AnyOf([ - T_USE => new Sequence([$type, new Token(';')], function($values) { return implode('', $values[1]); }), + T_USE => new Sequence([$type, new Token(';')], function($values) { + return strtr(implode('', $values[1]), '\\', '.'); + }), T_NEW => new Sequence([new Token(T_STRING), new Token('('), new Token(T_CONSTANT_ENCAPSED_STRING), new Token(')'), new Token(';')], function($values) { - return strtr(trim($values[3], '\'"'), '.', '\\'); + return trim($values[3], '\'"'); }) ]), ':uses_opt' => new AnyOf([ T_STRING => new Sequence([new Token('('), new SkipOver('(', ')'), new Token(';')], function($values) { if ('uses' === $values[0]) { - return array_map(function($class) { return strtr(trim($class, "'\" "), '.', '\\'); }, explode(',', $values[2])); + return array_map(function($class) { return trim($class, "'\" "); }, explode(',', $values[2])); } else { return null; } diff --git a/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php b/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php index ccf9745fda..47bb02c161 100755 --- a/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php +++ b/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php @@ -44,7 +44,7 @@ public function class_in_sub_namespace() { #[@test] public function class_with_import() { $this->assertEquals( - new CodeUnit('lang', ['util\Date'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), + new CodeUnit('lang', ['util.Date'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit('lang', ['util\Date', 'util\Objects'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), + new CodeUnit('lang', ['util.Date', 'util.Objects'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit('lang', ['xp\ArrayListExtensions'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), + new CodeUnit('lang', ['xp.ArrayListExtensions'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit('test', ['lang\Object', 'util\Date'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), + new CodeUnit('test', ['lang.Object', 'util.Date'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse('assertEquals( - new CodeUnit(null, ['lang\Object'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), + new CodeUnit(null, ['lang.Object'], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody())), (new PhpSyntax())->parse(' Date: Sun, 22 Feb 2015 14:57:06 +0100 Subject: [PATCH 10/23] Adjust to changes in imports --- src/main/php/lang/reflect/ClassParser.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/reflect/ClassParser.class.php b/src/main/php/lang/reflect/ClassParser.class.php index 3e0e85593f..d78c000a2f 100755 --- a/src/main/php/lang/reflect/ClassParser.class.php +++ b/src/main/php/lang/reflect/ClassParser.class.php @@ -302,7 +302,7 @@ public function parseDetails($bytes, $context= '') { $imports= []; foreach ($codeunit->imports() as $type) { - $imports[substr($type, strrpos($type, '\\')+ 1)]= strtr($type, '\\', '.'); + $imports[substr($type, strrpos($type, '.')+ 1)]= $type; } $decl= $codeunit->declaration(); From d9f3736df0138c9e04bbf08318b39c736ea174e0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Feb 2015 15:01:56 +0100 Subject: [PATCH 11/23] Replace AnyOf -> Repeated(OneOf()) --- src/main/php/lang/codedom/AnyOf.class.php | 32 ------------------- src/main/php/lang/codedom/PhpSyntax.class.php | 8 ++--- src/test/config/unittest/core.ini | 3 ++ 3 files changed, 7 insertions(+), 36 deletions(-) delete mode 100755 src/main/php/lang/codedom/AnyOf.class.php diff --git a/src/main/php/lang/codedom/AnyOf.class.php b/src/main/php/lang/codedom/AnyOf.class.php deleted file mode 100755 index aeeb50782b..0000000000 --- a/src/main/php/lang/codedom/AnyOf.class.php +++ /dev/null @@ -1,32 +0,0 @@ -cases= $cases; - } - - public function consume($rules, $stream, $values) { - $begin= $stream->position(); - - $match= true; - do { - $case= $stream->token(); - if (isset($this->cases[$case[0]])) { - $stream->next(); - $result= $this->cases[$case[0]]->consume($rules, $stream, [is_array($case) ? $case[1] : $case]); - if ($result->matched()) { - $values[]= $result->backing(); - } else { - $stream->reset($begin); - return new Unexpected('Matched beginning but not rest: '.$result->error(), $stream->line()); - } - } else { - $match= false; - } - } while ($match); - - return new Values($values); - } -} \ No newline at end of file diff --git a/src/main/php/lang/codedom/PhpSyntax.class.php b/src/main/php/lang/codedom/PhpSyntax.class.php index c7fabe1234..2707374c99 100755 --- a/src/main/php/lang/codedom/PhpSyntax.class.php +++ b/src/main/php/lang/codedom/PhpSyntax.class.php @@ -25,15 +25,15 @@ static function __static() { return strtr(implode('', $values[1]), '\\', '.'); }) ])), - ':imports' => new AnyOf([ + ':imports' => new Repeated(new OneOf([ T_USE => new Sequence([$type, new Token(';')], function($values) { return strtr(implode('', $values[1]), '\\', '.'); }), T_NEW => new Sequence([new Token(T_STRING), new Token('('), new Token(T_CONSTANT_ENCAPSED_STRING), new Token(')'), new Token(';')], function($values) { return trim($values[3], '\'"'); }) - ]), - ':uses_opt' => new AnyOf([ + ])), + ':uses_opt' => new Repeated(new OneOf([ T_STRING => new Sequence([new Token('('), new SkipOver('(', ')'), new Token(';')], function($values) { if ('uses' === $values[0]) { return array_map(function($class) { return trim($class, "'\" "); }, explode(',', $values[2])); @@ -41,7 +41,7 @@ static function __static() { return null; } }) - ]), + ])), ':declaration' => new Sequence( [ new Rule(':annotations'), diff --git a/src/test/config/unittest/core.ini b/src/test/config/unittest/core.ini index 1a4f7d4c6f..15ad2452cd 100644 --- a/src/test/config/unittest/core.ini +++ b/src/test/config/unittest/core.ini @@ -263,3 +263,6 @@ class="net.xp_framework.unittest.reflection.ModuleTest" [modules:loading] class="net.xp_framework.unittest.reflection.ModuleLoadingTest" + +[codedom] +class="net.xp_framework.unittest.core.PhpSyntaxTest" \ No newline at end of file From 2aaa01e8a20779b851eeb9927b6af1385f168c64 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Feb 2015 15:10:58 +0100 Subject: [PATCH 12/23] Extract into base class --- src/main/php/lang/codedom/PhpSyntax.class.php | 16 +------------- src/main/php/lang/codedom/Syntax.class.php | 22 +++++++++++++++++++ .../php/lang/reflect/ClassParser.class.php | 6 +---- 3 files changed, 24 insertions(+), 20 deletions(-) create mode 100755 src/main/php/lang/codedom/Syntax.class.php diff --git a/src/main/php/lang/codedom/PhpSyntax.class.php b/src/main/php/lang/codedom/PhpSyntax.class.php index 2707374c99..1d0e09aaa1 100755 --- a/src/main/php/lang/codedom/PhpSyntax.class.php +++ b/src/main/php/lang/codedom/PhpSyntax.class.php @@ -1,9 +1,6 @@ evaluate(self::$parse, new Stream($input)); - } } \ No newline at end of file diff --git a/src/main/php/lang/codedom/Syntax.class.php b/src/main/php/lang/codedom/Syntax.class.php new file mode 100755 index 0000000000..ac4ad836e8 --- /dev/null +++ b/src/main/php/lang/codedom/Syntax.class.php @@ -0,0 +1,22 @@ +evaluate(self::$parse, new Stream($input)); + } +} \ No newline at end of file diff --git a/src/main/php/lang/reflect/ClassParser.class.php b/src/main/php/lang/reflect/ClassParser.class.php index d78c000a2f..5f01a77d78 100755 --- a/src/main/php/lang/reflect/ClassParser.class.php +++ b/src/main/php/lang/reflect/ClassParser.class.php @@ -307,11 +307,7 @@ public function parseDetails($bytes, $context= '') { $decl= $codeunit->declaration(); $annotations= $this->parseAnnotations($decl->annotations(), $context, $imports, -1); - $details= [ - 0 => [], // Fields - 1 => [], // Methods - 'class' => [DETAIL_ANNOTATIONS => $annotations[0]] - ]; + $details= [0 => [], 1 => [], 'class' => [DETAIL_ANNOTATIONS => $annotations[0]]]; foreach ($decl->body()->members() as $member) { $annotations= $this->parseAnnotations($member->annotations(), $context, $imports, -1); From 9340eea6b15437e779b41714aae6ed9a4aea0bd0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Feb 2015 19:58:33 +0100 Subject: [PATCH 13/23] Initial release: Annotation parser --- .../lang/codedom/AnnotationArray.class.php | 43 +++++ .../lang/codedom/AnnotationClosure.class.php | 50 ++++++ .../lang/codedom/AnnotationConstant.class.php | 35 ++++ .../codedom/AnnotationDeclaration.class.php | 40 +++++ .../lang/codedom/AnnotationInstance.class.php | 46 +++++ .../lang/codedom/AnnotationMember.class.php | 54 ++++++ .../lang/codedom/AnnotationPairs.class.php | 38 +++++ .../lang/codedom/AnnotationSyntax.class.php | 101 +++++++++++ .../lang/codedom/AnnotationValue.class.php | 34 ++++ .../php/lang/codedom/Annotations.class.php | 29 ++++ src/main/php/lang/codedom/OneOf.class.php | 6 +- src/main/php/lang/codedom/PhpSyntax.class.php | 2 +- .../php/lang/codedom/ResolvingType.class.php | 31 ++++ src/main/php/lang/codedom/Syntax.class.php | 5 +- .../core/AnnotationSyntaxTest.class.php | 161 ++++++++++++++++++ 15 files changed, 671 insertions(+), 4 deletions(-) create mode 100755 src/main/php/lang/codedom/AnnotationArray.class.php create mode 100755 src/main/php/lang/codedom/AnnotationClosure.class.php create mode 100755 src/main/php/lang/codedom/AnnotationConstant.class.php create mode 100755 src/main/php/lang/codedom/AnnotationDeclaration.class.php create mode 100755 src/main/php/lang/codedom/AnnotationInstance.class.php create mode 100755 src/main/php/lang/codedom/AnnotationMember.class.php create mode 100755 src/main/php/lang/codedom/AnnotationPairs.class.php create mode 100755 src/main/php/lang/codedom/AnnotationSyntax.class.php create mode 100755 src/main/php/lang/codedom/AnnotationValue.class.php create mode 100755 src/main/php/lang/codedom/Annotations.class.php create mode 100755 src/main/php/lang/codedom/ResolvingType.class.php create mode 100755 src/test/php/net/xp_framework/unittest/core/AnnotationSyntaxTest.class.php diff --git a/src/main/php/lang/codedom/AnnotationArray.class.php b/src/main/php/lang/codedom/AnnotationArray.class.php new file mode 100755 index 0000000000..b1e8880585 --- /dev/null +++ b/src/main/php/lang/codedom/AnnotationArray.class.php @@ -0,0 +1,43 @@ +backing= $value; + } + + public function resolve($context, $imports) { + $resolved= []; + foreach ($this->backing as $value) { + $k= key($value); + if (0 === $k) { + $resolved[]= current($value)->resolve($context, $imports); + } else { + $resolved[$k]= current($value)->resolve($context, $imports); + } + } + return $resolved; + } + + /** + * Creates a string representation + * + * @return string + */ + public function toString() { + return $this->getClassName().'('.Objects::stringOf($this->backing).')'; + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && Objects::equal($this->backing, $cmp->backing); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/AnnotationClosure.class.php b/src/main/php/lang/codedom/AnnotationClosure.class.php new file mode 100755 index 0000000000..8244c79d43 --- /dev/null +++ b/src/main/php/lang/codedom/AnnotationClosure.class.php @@ -0,0 +1,50 @@ +signature= $signature; + $this->code= $code; + } + + public function resolve($context, $imports) { + $func= eval('return function('.$this->signature.') {'.$this->code.'};'); + if (!($func instanceof \Closure)) { + if ($error= error_get_last()) { + set_error_handler('__error', 0); + trigger_error('clear_last_error'); + restore_error_handler(); + } else { + $error= ['message' => 'Syntax error']; + } + throw new IllegalStateException('In `'.$this->code.'`: '.ucfirst($error['message'])); + } + return $func; + } + + /** + * Creates a string representation + * + * @return string + */ + public function toString() { + return $this->getClassName().'('.Objects::stringOf($this->backing).')'; + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && ( + $this->signature === $cmp->signature && + $this->code === $cmp->code + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/AnnotationConstant.class.php b/src/main/php/lang/codedom/AnnotationConstant.class.php new file mode 100755 index 0000000000..cf95c6195f --- /dev/null +++ b/src/main/php/lang/codedom/AnnotationConstant.class.php @@ -0,0 +1,35 @@ +name= $name; + } + + public function resolve($context, $imports) { + if (defined($this->name)) { + return constant($this->name); + } + raise('lang.ElementNotFoundException', 'Undefined constant "'.$this->name.'"'); + } + + /** + * Creates a string representation + * + * @return string + */ + public function toString() { + return $this->getClassName().'('.$this->name.')'; + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && $this->name === $cmp->name; + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/AnnotationDeclaration.class.php b/src/main/php/lang/codedom/AnnotationDeclaration.class.php new file mode 100755 index 0000000000..9388b4c3d4 --- /dev/null +++ b/src/main/php/lang/codedom/AnnotationDeclaration.class.php @@ -0,0 +1,40 @@ +target= $target; + $this->name= $name; + $this->value= $value; + } + + /** @return string */ + public function target() { return $this->target; } + + /** @return string */ + public function name() { return $this->name; } + + /** @return string */ + public function value() { return $this->value; } + + public function resolve($context, $imports) { + return $this->value->resolve($context, $imports); + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && ( + $this->target === $cmp->target && + $this->name === $cmp->name && + Objects::equal($this->value, $cmp->value) + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/AnnotationInstance.class.php b/src/main/php/lang/codedom/AnnotationInstance.class.php new file mode 100755 index 0000000000..853ed3da84 --- /dev/null +++ b/src/main/php/lang/codedom/AnnotationInstance.class.php @@ -0,0 +1,46 @@ +type= $type; + $this->arguments= $arguments; + } + + public function resolve($context, $imports) { + $type= $this->typeOf($this->type, $context, $imports); + if ($type->hasConstructor()) { + return $type->getConstructor()->newInstance(array_map( + function($arg) use($context, $imports) { return $arg->resolve($context, $imports); }, + $this->arguments + )); + } else { + return $type->newInstance(); + } + } + + /** + * Creates a string representation + * + * @return string + */ + public function toString() { + return $this->getClassName().'(new '.$this->type.'('.Objects::stringOf($this->arguments).'))'; + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && ( + Objects::equal($this->arguments, $cmp->arguments) && + Objects::equal($this->type, $cmp->type) + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/AnnotationMember.class.php b/src/main/php/lang/codedom/AnnotationMember.class.php new file mode 100755 index 0000000000..47034adc24 --- /dev/null +++ b/src/main/php/lang/codedom/AnnotationMember.class.php @@ -0,0 +1,54 @@ +type= $type; + $this->name= $name; + } + + public function resolve($context, $imports) { + $type= $this->typeOf($this->type, $context, $imports); + if ('$' === $this->name{0}) { + $field= $type->getField(substr($this->name, 1)); + $m= $field->getModifiers(); + if ($m & MODIFIER_PUBLIC) { + return $field->get(null); + } else if (($m & MODIFIER_PROTECTED) && $type->isAssignableFrom($context)) { + return $field->setAccessible(true)->get(null); + } else if (($m & MODIFIER_PRIVATE) && $type->getName() === $context) { + return $field->setAccessible(true)->get(null); + } else { + throw new IllegalAccessException(sprintf( + 'Cannot access %s field %s::$%s', + implode(' ', Modifiers::namesOf($m)), + $type->getName(), + $field->getName() + )); + } + return $type->getField(substr($this->name, 1))->setAccessible(true)->get(null); + } else if ('class' === $this->name) { + return ltrim($type->literal(), '\\'); + } else { + return $type->getConstant($this->name); + } + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && ( + $this->name === $cmp->name && + $this->type === $cmp->type + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/AnnotationPairs.class.php b/src/main/php/lang/codedom/AnnotationPairs.class.php new file mode 100755 index 0000000000..3b53de02e9 --- /dev/null +++ b/src/main/php/lang/codedom/AnnotationPairs.class.php @@ -0,0 +1,38 @@ +backing= $value; + } + + public function resolve($context, $imports) { + $resolved= []; + foreach ($this->backing as $pair) { + $resolved[key($pair)]= current($pair)->resolve($context, $imports); + } + return $resolved; + } + + /** + * Creates a string representation + * + * @return string + */ + public function toString() { + return $this->getClassName().'('.Objects::stringOf($this->backing).')'; + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && Objects::equal($this->backing, $cmp->backing); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/AnnotationSyntax.class.php b/src/main/php/lang/codedom/AnnotationSyntax.class.php new file mode 100755 index 0000000000..27589e6970 --- /dev/null +++ b/src/main/php/lang/codedom/AnnotationSyntax.class.php @@ -0,0 +1,101 @@ + new Sequence([], function($values) { return (double)$values[0]; }), + T_LNUMBER => new Sequence([], function($values) { return (int)$values[0]; }) + ]); + $key= new OneOf([ + T_STRING => new Sequence([], function($values) { return $values[0]; }), + T_RETURN => new Returns('return'), + T_CLASS => new Returns('class'), + T_IMPLEMENTS => new Returns('implements'), + T_LIST => new Returns('list') + ]); + $type= new Tokens([T_STRING, T_NS_SEPARATOR]); + + self::$parse[__CLASS__]= [ + ':start' => new Sequence([new Token(T_OPEN_TAG), new Token('['), new ListOf(new Rule(':annotation')), new Token(']')], function($values) { + return new Annotations($values[2]); + }), + ':annotation' => new Sequence([new Token('@'), new Rule(':target_opt'), new Token(T_STRING), new Rule(':value_opt')], function($values) { + return new AnnotationDeclaration($values[1], $values[2], $values[3] ?: new AnnotationValue(null)); + }), + ':target_opt' => new Optional(new Sequence( + [new Token(T_VARIABLE), new Token(':')], + function($values) { return substr($values[0], 1); } + )), + ':value_opt' => new Optional(new Sequence( + [new Token('('), new Rule(':expr'), new Token(')')], + function($values) { return $values[1]; } + )), + ':expr' => new EitherOf([ + new Sequence([$number], function($values) { + return new AnnotationValue($values[0]); + }), + new Sequence([new Token('-'), $number], function($values) { + return new AnnotationValue(-1 * $values[1]); + }), + new Sequence([new Token('+'), $number], function($values) { + return new AnnotationValue($values[1]); + }), + new Sequence([new Token(T_CONSTANT_ENCAPSED_STRING)], function($values) { + return new AnnotationValue(eval('return '.$values[0].';')); + }), + new Sequence([new Token('['), new Optional(new ListOf(new Rule(':element'))), new Token(']')], function($values) { + return new AnnotationArray((array)$values[1]); + }), + new Sequence([new Token(T_ARRAY), new Token('('), new Optional(new ListOf(new Rule(':element'))), new Token(')')], function($values) { + return new AnnotationArray((array)$values[2]); + }), + new Sequence([new Token(T_NEW), $type, new Token('('), new Optional(new ListOf(new Rule(':expr'))), new Token(')')], function($values) { + return new AnnotationInstance(implode('', $values[1]), (array)$values[3]); + }), + new Sequence([new Token(T_FUNCTION), new Token('('), new SkipOver('(', ')'), new Token('{'), new SkipOver('{', '}')], function($values) { + return new AnnotationClosure($values[2], $values[4]); + }), + new Sequence([$type, new Token(T_DOUBLE_COLON), new Rule(':member')], function($values) { + return new AnnotationMember(implode('', $values[0]), $values[2]); + }), + new Sequence([new ListOf(new Rule(':pair'))], function($values) { + return new AnnotationPairs($values[0]); + }), + new Sequence([new Token(T_STRING)], function($values) { + return new AnnotationConstant($values[0]); + }) + ]), + ':pair' => new Sequence([$key, new Token('='), new Rule(':expr')], function($values) { + return [$values[0] => $values[2]]; + }), + ':element' => new EitherOf([ + new Sequence([new Token(T_CONSTANT_ENCAPSED_STRING), new Token(T_DOUBLE_ARROW), new Rule(':expr')], function($values) { + return [trim($values[0], '"\'') => $values[2]]; + }), + new Sequence([new Rule(':expr')], function($values) { + return [0 => $values[0]]; + }) + ]), + ':member' => new Sequence( + [new OneOf([ + T_STRING => new Sequence([], function($values) { return $values[0]; }), + T_CLASS => new Sequence([], function($values) { return $values[0]; }), + T_VARIABLE => new Sequence([], function($values) { return $values[0]; }), + ])], + function($values) { return $values[0]; } + ) + ]; + } + + /** + * Parses input + * + * @param string $input + * @return var + * @throws lang.FormatException + */ + public function parse($input) { + return parent::parse('backing= $value; + } + + public function resolve($context, $imports) { + return $this->backing; + } + + /** + * Creates a string representation + * + * @return string + */ + public function toString() { + return $this->getClassName().'('.Objects::stringOf($this->backing).')'; + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && Objects::equal($this->backing, $cmp->backing); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Annotations.class.php b/src/main/php/lang/codedom/Annotations.class.php new file mode 100755 index 0000000000..b2b8a6b648 --- /dev/null +++ b/src/main/php/lang/codedom/Annotations.class.php @@ -0,0 +1,29 @@ +list= $list; + } + + public function resolve($context, $imports= []) { + $resolved= []; + foreach ($this->list as $annotation) { + $resolved[$annotation->target()][$annotation->name()]= $annotation->resolve($context, $imports); + } + return $resolved; + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && Objects::equal($this->list, $cmp->list); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/OneOf.class.php b/src/main/php/lang/codedom/OneOf.class.php index 40c3f8c13a..4a462b7414 100755 --- a/src/main/php/lang/codedom/OneOf.class.php +++ b/src/main/php/lang/codedom/OneOf.class.php @@ -9,6 +9,10 @@ public function __construct($cases) { $this->cases= $cases; } + private function tokenName($token) { + return is_int($token) ? token_name($token) : '`'.$token.'`'; + } + public function consume($rules, $stream, $values) { $case= $stream->token(); if (isset($this->cases[$case[0]])) { @@ -20,7 +24,7 @@ public function consume($rules, $stream, $values) { sprintf( 'Unexpected %s, expecting one of %s', is_array($case) ? token_name($case[0]) : $case, - implode(', ', array_map('token_name', array_keys($this->cases))) + implode(', ', array_map([$this, 'tokenName'], array_keys($this->cases))) ), $stream->line() ); diff --git a/src/main/php/lang/codedom/PhpSyntax.class.php b/src/main/php/lang/codedom/PhpSyntax.class.php index 1d0e09aaa1..8710565eee 100755 --- a/src/main/php/lang/codedom/PhpSyntax.class.php +++ b/src/main/php/lang/codedom/PhpSyntax.class.php @@ -6,7 +6,7 @@ static function __static() { $type= new Tokens([T_STRING, T_NS_SEPARATOR]); $modifiers= new Tokens([T_PUBLIC, T_PRIVATE, T_PROTECTED, T_STATIC, T_FINAL, T_ABSTRACT, T_WHITESPACE]); - self::$parse= [ + self::$parse[__CLASS__]= [ ':start' => new Sequence([new Token(T_OPEN_TAG), new Rule(':namespace'), new Rule(':imports'), new Rule(':uses_opt'), new Rule(':declaration')], function($values) { $imports= $values[2]; foreach ($values[3] as $uses) { diff --git a/src/main/php/lang/codedom/ResolvingType.class.php b/src/main/php/lang/codedom/ResolvingType.class.php new file mode 100755 index 0000000000..0a6c1aca16 --- /dev/null +++ b/src/main/php/lang/codedom/ResolvingType.class.php @@ -0,0 +1,31 @@ + fully qualified class names + * @return lang.XPClass + */ + protected function typeOf($type, $context, $imports) { + if ('self' === $type) { + return XPClass::forName($context); + } else if ('parent' === $type) { + return XPClass::forName($context)->getParentclass(); + } else if (isset($imports[$type])) { + return XPClass::forName($imports[$type]); + } else if (strstr($type, '\\')) { + return XPClass::forName(ltrim(strtr($type, '\\', '.'), '.')); + } else if (class_exists($type, false) || interface_exists($type, false)) { + return new XPClass($type); + } else if (false !== ($p= strrpos($context, '.'))) { + return XPClass::forName(substr($context, 0, $p + 1).$type); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/Syntax.class.php b/src/main/php/lang/codedom/Syntax.class.php index ac4ad836e8..79b5f29b76 100755 --- a/src/main/php/lang/codedom/Syntax.class.php +++ b/src/main/php/lang/codedom/Syntax.class.php @@ -2,7 +2,7 @@ /** * Base class for syntaxes. Subclasses initialize the "parse" member - * in their static initializer. + * in their static initializer with `self::$parse[__CLASS__]= ...`. * * @see xp://lang.codedom.PhpSyntax */ @@ -17,6 +17,7 @@ abstract class Syntax extends \lang\Object { * @throws lang.FormatException */ public function parse($input) { - return self::$parse[':start']->evaluate(self::$parse, new Stream($input)); + $rules= self::$parse[get_class($this)]; + return $rules[':start']->evaluate($rules, new Stream($input)); } } \ No newline at end of file diff --git a/src/test/php/net/xp_framework/unittest/core/AnnotationSyntaxTest.class.php b/src/test/php/net/xp_framework/unittest/core/AnnotationSyntaxTest.class.php new file mode 100755 index 0000000000..f1941f5fb5 --- /dev/null +++ b/src/test/php/net/xp_framework/unittest/core/AnnotationSyntaxTest.class.php @@ -0,0 +1,161 @@ +assertEquals( + [null => ['test' => null]], + (new AnnotationSyntax())->parse('[@test]')->resolve($this->getClassName()) + ); + } + + #[@test] + public function target_annotation_without_value() { + $this->assertEquals( + ['param' => ['test' => null]], + (new AnnotationSyntax())->parse('[@$param: test]')->resolve($this->getClassName()) + ); + } + + #[@test] + public function two_annotations_without_value() { + $this->assertEquals( + [null => ['test' => null, 'ignore' => null]], + (new AnnotationSyntax())->parse('[@test, @ignore]')->resolve($this->getClassName()) + ); + } + + #[@test, @values([ + # ['1.5', 1.5], ['-1.5', -1.5], ['+1.5', +1.5], + # ['1', 1], ['0', 0], ['-6100', -6100], ['+6100', +6100], + # ['true', true], ['false', false], ['null', null], + # ['""', ''], ["''", ''], ['"Test"', 'Test'], ["'Test'", 'Test'] + #])] + public function annotation_with_primitive_values($literal, $value) { + $this->assertEquals( + [null => ['test' => $value]], + (new AnnotationSyntax())->parse('[@test('.$literal.')]')->resolve($this->getClassName()) + ); + } + + #[@test, @values([ + # ['[]', []], ['[1, 2, 3]', [1, 2, 3]], + # ['array()', []], ['array(1, 2, 3)', [1, 2, 3]] + #])] + public function annotation_with_arrays($literal, $value) { + $this->assertEquals( + [null => ['test' => $value]], + (new AnnotationSyntax())->parse('[@test('.$literal.')]')->resolve($this->getClassName()) + ); + } + + #[@test, @values([ + # ['["color" => "green"]', ['color' => 'green']], + # ['["a" => "b", "c" => "d"]', ['a' => 'b', 'c' => 'd']], + # ['array("a" => "b", "c" => "d")', ['a' => 'b', 'c' => 'd']] + #])] + public function annotation_with_maps($literal, $value) { + $this->assertEquals( + [null => ['test' => $value]], + (new AnnotationSyntax())->parse('[@test('.$literal.')]')->resolve($this->getClassName()) + ); + } + + #[@test, @values([ + # ['function() { }', 'function(): var'], + # ['function($a) { }', 'function(var): var'], + # ['function($a, array $b) { }', 'function(var, var[]): var'] + #])] + public function annotation_with_closures($literal, $type) { + $annotation= (new AnnotationSyntax())->parse('[@test('.$literal.')]')->resolve($this->getClassName()); + $this->assertInstanceOf($type, $annotation[null]['test']); + } + + #[@test, @values([ + # ['limit=1.5', ['limit' => 1.5]], + # ['limit=1.5, eta=1.0', ['limit' => 1.5, 'eta' => 1.0]], + # ['limit=[1, 2, 3]', ['limit' => [1, 2, 3]]], + # ['class="Test"', ['class' => "Test"]], + # ['return="Test"', ['return' => "Test"]], + # ['self="Test"', ['self' => "Test"]], + # ['implements="Test"', ['implements' => "Test"]] + #])] + public function annotation_with_key_value_pairs($literal, $value) { + $this->assertEquals( + [null => ['test' => $value]], + (new AnnotationSyntax())->parse('[@test('.$literal.')]')->resolve($this->getClassName()) + ); + } + + #[@test, @values([ + # 'self::CONSTANT', + # '\net\xp_framework\unittest\core\AnnotationSyntaxTest::CONSTANT', + # 'AnnotationSyntaxTest::CONSTANT' + #])] + public function annotation_with_type_constant($literal) { + $this->assertEquals( + [null => ['test' => self::CONSTANT]], + (new AnnotationSyntax())->parse('[@test('.$literal.')]')->resolve($this->getClassName()) + ); + } + + #[@test, @values([ + # 'self::class', + # '\net\xp_framework\unittest\core\AnnotationSyntaxTest::class', + # 'AnnotationSyntaxTest::class' + #])] + public function annotation_with_class_constant($literal) { + $this->assertEquals( + [null => ['test' => __CLASS__]], + (new AnnotationSyntax())->parse('[@test('.$literal.')]')->resolve($this->getClassName()) + ); + } + + #[@test, @values([ + # 'self::$STATIC', + # '\net\xp_framework\unittest\core\AnnotationSyntaxTest::$STATIC', + # 'AnnotationSyntaxTest::$STATIC' + #])] + public function annotation_with_static_member($literal) { + $this->assertEquals( + [null => ['test' => self::$STATIC]], + (new AnnotationSyntax())->parse('[@test('.$literal.')]')->resolve($this->getClassName()) + ); + } + + #[@test] + public function annotation_with_new() { + $this->assertEquals( + [null => ['test' => new String('Test')]], + (new AnnotationSyntax())->parse('[@test(new String("Test"))]')->resolve($this->getClassName()) + ); + } + + #[@test] + public function annotation_with_imported() { + $this->assertEquals( + [null => ['test' => new Long(6100)]], + (new AnnotationSyntax())->parse('[@test(new Long(6100))]')->resolve($this->getClassName(), [ + 'Long' => 'lang.types.Long' + ]) + ); + } + + #[@test] + public function annotation_with_new_and_fully_qualified_type() { + $this->assertEquals( + [null => ['test' => new String('Test')]], + (new AnnotationSyntax())->parse('[@test(new \lang\types\String("Test"))]')->resolve($this->getClassName()) + ); + } +} From cecac62208380f03f5e529bef1d4406e7c5137f1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Feb 2015 19:58:53 +0100 Subject: [PATCH 14/23] Refactor: Use new Annotation parser instead of handcrafted one --- .../php/lang/reflect/ClassParser.class.php | 289 ++---------------- src/test/config/unittest/core.ini | 7 +- .../AnnotationParsingTest.class.php | 2 +- .../BrokenAnnotationTest.class.php | 61 ++-- 4 files changed, 57 insertions(+), 302 deletions(-) diff --git a/src/main/php/lang/reflect/ClassParser.class.php b/src/main/php/lang/reflect/ClassParser.class.php index 5f01a77d78..bd7bd41d3b 100755 --- a/src/main/php/lang/reflect/ClassParser.class.php +++ b/src/main/php/lang/reflect/ClassParser.class.php @@ -1,8 +1,8 @@ getParentclass(); - } else if (false !== strpos($type, '.')) { - return XPClass::forName($type); - } else if (isset($imports[$type])) { - return XPClass::forName($imports[$type]); - } else if (class_exists($type, false) || interface_exists($type, false)) { - return new XPClass($type); - } else if (false !== ($p= strrpos($context, '.'))) { - return XPClass::forName(substr($context, 0, $p + 1).$type); - } - } - - /** - * Resolves a class member, which is either a field, a class constant - * or the `ClassName::class` syntax, which returns the class' literal. - * - * @param lang.XPClass $class - * @param var[] $token A token as returned by `token_get_all()` - * @param string $context - * @return var - */ - protected function memberOf($class, $token, $context) { - if (T_VARIABLE === $token[0]) { - $field= $class->getField(substr($token[1], 1)); - $m= $field->getModifiers(); - if ($m & MODIFIER_PUBLIC) { - return $field->get(null); - } else if (($m & MODIFIER_PROTECTED) && $class->isAssignableFrom($context)) { - return $field->setAccessible(true)->get(null); - } else if (($m & MODIFIER_PRIVATE) && $class->getName() === $context) { - return $field->setAccessible(true)->get(null); - } else { - throw new IllegalAccessException(sprintf( - 'Cannot access %s field %s::$%s', - implode(' ', Modifiers::namesOf($m)), - $class->getName(), - $field->getName() - )); - } - } else if (T_CLASS === $token[0]) { - return $class->literal(); - } else { - return $class->getConstant($token[1]); - } - } - - /** - * Parses a single value, recursively, if necessary - * - * @param var[] $tokens - * @param int $i - * @param string $context - * @param [:string] $imports - * @return var - */ - protected function valueOf($tokens, &$i, $context, $imports) { - if ('-' === $tokens[$i][0]) { - $i++; - return -1 * $this->valueOf($tokens, $i, $context, $imports); - } else if ('+' === $tokens[$i][0]) { - $i++; - return +1 * $this->valueOf($tokens, $i, $context, $imports); - } else if (T_CONSTANT_ENCAPSED_STRING === $tokens[$i][0]) { - return eval('return '.$tokens[$i][1].';'); - } else if (T_LNUMBER === $tokens[$i][0]) { - return (int)$tokens[$i][1]; - } else if (T_DNUMBER === $tokens[$i][0]) { - return (double)$tokens[$i][1]; - } else if ('[' === $tokens[$i] || T_ARRAY === $tokens[$i][0]) { - $value= []; - $element= null; - $key= 0; - $end= '[' === $tokens[$i] ? ']' : ')'; - for ($i++, $s= sizeof($tokens); ; $i++) { - if ($i >= $s) { - throw new IllegalStateException('Parse error: Unterminated array'); - } else if ($end === $tokens[$i]) { - $element && $value[$key]= $element[0]; - break; - } else if ('(' === $tokens[$i]) { - // Skip - } else if (',' === $tokens[$i]) { - $element || raise('lang.IllegalStateException', 'Parse error: Malformed array - no value before comma'); - $value[$key]= $element[0]; - $element= null; - $key= sizeof($value); - } else if (T_DOUBLE_ARROW === $tokens[$i][0]) { - $key= $element[0]; - $element= null; - } else if (T_WHITESPACE === $tokens[$i][0]) { - continue; - } else { - $element && raise('lang.IllegalStateException', 'Parse error: Malformed array - missing comma'); - $element= [$this->valueOf($tokens, $i, $context, $imports)]; - } - } - return $value; - } else if ('"' === $tokens[$i] || T_ENCAPSED_AND_WHITESPACE === $tokens[$i][0]) { - throw new IllegalStateException('Parse error: Unterminated string'); - } else if (T_NS_SEPARATOR === $tokens[$i][0]) { - $type= ''; - while (T_NS_SEPARATOR === $tokens[$i++][0]) { - $type.= '.'.$tokens[$i++][1]; - } - return $this->memberOf(XPClass::forName(substr($type, 1)), $tokens[$i], $context); - } else if (T_STRING === $tokens[$i][0]) { // constant vs. class::constant - if (T_DOUBLE_COLON === $tokens[$i + 1][0]) { - $i+= 2; - return $this->memberOf($this->resolve($tokens[$i - 2][1], $context, $imports), $tokens[$i], $context); - } else if (defined($tokens[$i][1])) { - return constant($tokens[$i][1]); - } else { - raise('lang.ElementNotFoundException', 'Undefined constant "'.$tokens[$i][1].'"'); - } - } else if (T_NEW === $tokens[$i][0]) { - $type= ''; - while ('(' !== $tokens[$i++]) { - if (T_STRING === $tokens[$i][0]) $type.= '.'.$tokens[$i][1]; - } - $class= $this->resolve(substr($type, 1), $context, $imports); - for ($args= [], $arg= null, $s= sizeof($tokens); ; $i++) { - if (')' === $tokens[$i]) { - $arg && $args[]= $arg[0]; - break; - } else if (',' === $tokens[$i]) { - $args[]= $arg[0]; - $arg= null; - } else if (T_WHITESPACE !== $tokens[$i][0]) { - $arg= [$this->valueOf($tokens, $i, $context, $imports)]; - } - } - return $class->hasConstructor() ? $class->getConstructor()->newInstance($args) : $class->newInstance(); - } else if (T_FUNCTION === $tokens[$i][0]) { - $b= 0; - $code= 'function'; - for ($i++, $s= sizeof($tokens); $i < $s; $i++) { - if ('{' === $tokens[$i]) { - $b++; - $code.= '{'; - } else if ('}' === $tokens[$i]) { - $b--; - $code.= '}'; - if (0 === $b) break; - } else { - $code.= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; - } - } - $func= eval('return '.$code.';'); - if (!($func instanceof \Closure)) { - if ($error= error_get_last()) { - set_error_handler('__error', 0); - trigger_error('clear_last_error'); - restore_error_handler(); - } else { - $error= ['message' => 'Syntax error']; - } - throw new IllegalStateException('In `'.$code.'`: '.ucfirst($error['message'])); - } - return $func; - } else { - throw new IllegalStateException(sprintf( - 'Parse error: Unexpected %s', - is_array($tokens[$i]) ? token_name($tokens[$i][0]) : '"'.$tokens[$i].'"' - )); - } - } - - /** - * Parses annotation string + * Parse details from a given input string * - * @param string bytes - * @param string context the class name - * @return [:string] imports - * @param int line - * @return [:var] - * @throws lang.ClassFormatException + * @param string $bytes + * @param string $context + * @param [:string] $imports + * @return [:var] details */ - public function parseAnnotations($bytes, $context, $imports= [], $line= -1) { - static $states= [ - 'annotation', 'annotation name', 'annotation value', - 'annotation map key', 'annotation map value', - 'multi-value' - ]; - + public function parseAnnotations($bytes, $context, $imports= []) { $annotations= [0 => [], 1 => []]; if (null === $bytes) return $annotations; - $tokens= token_get_all('valueOf($tokens, $i, $context, $imports); - } - } else if (3 === $state) { // Parsing key inside @attr(a= b, c= d) - if (')' === $tokens[$i]) { - $state= 1; - } else if (',' === $tokens[$i]) { - $key= null; - } else if ('=' === $tokens[$i]) { - $state= 4; - } else if (is_array($tokens[$i])) { - $key= $tokens[$i][1]; - } - } else if (4 === $state) { // Parsing value inside @attr(a= b, c= d) - $value[$key]= $this->valueOf($tokens, $i, $context, $imports); - $state= 3; - } - } - } catch (\lang\XPException $e) { - raise('lang.ClassFormatException', $e->getMessage().' in '.$place, $e); + $parsed= (new AnnotationSyntax())->parse(trim($bytes, "# \t\n\r"))->resolve($context, $imports); + } catch (XPException $e) { + raise('lang.ClassFormatException', $e->getMessage().' in '.$context, $e); + } + + isset($parsed[null]) && $annotations[0]= $parsed[null]; + foreach ($parsed as $key => $value) { + $key && $annotations[1]['$'.$key]= $value; } - raise('lang.ClassFormatException', 'Parse error: Unterminated '.$states[$state].' in '.$place); + return $annotations; } /** @@ -298,7 +49,7 @@ public function parseAnnotations($bytes, $context, $imports= [], $line= -1) { * @return [:var] details */ public function parseDetails($bytes, $context= '') { - $codeunit= (new \lang\codedom\PhpSyntax())->parse(ltrim($bytes)); + $codeunit= (new PhpSyntax())->parse(ltrim($bytes)); $imports= []; foreach ($codeunit->imports() as $type) { @@ -310,7 +61,7 @@ public function parseDetails($bytes, $context= '') { $details= [0 => [], 1 => [], 'class' => [DETAIL_ANNOTATIONS => $annotations[0]]]; foreach ($decl->body()->members() as $member) { - $annotations= $this->parseAnnotations($member->annotations(), $context, $imports, -1); + $annotations= $this->parseAnnotations($member->annotations(), $context, $imports); if ($member->isField()) { $details[0][$member->name()]= [DETAIL_ANNOTATIONS => $annotations[0]]; } else if ($member->isMethod()) { diff --git a/src/test/config/unittest/core.ini b/src/test/config/unittest/core.ini index 15ad2452cd..3c1831d3fa 100644 --- a/src/test/config/unittest/core.ini +++ b/src/test/config/unittest/core.ini @@ -264,5 +264,8 @@ class="net.xp_framework.unittest.reflection.ModuleTest" [modules:loading] class="net.xp_framework.unittest.reflection.ModuleLoadingTest" -[codedom] -class="net.xp_framework.unittest.core.PhpSyntaxTest" \ No newline at end of file +[codedom.class] +class="net.xp_framework.unittest.core.PhpSyntaxTest" + +[codedom.annotations] +class="net.xp_framework.unittest.core.AnnotationSyntaxTest" \ No newline at end of file diff --git a/src/test/php/net/xp_framework/unittest/annotations/AnnotationParsingTest.class.php b/src/test/php/net/xp_framework/unittest/annotations/AnnotationParsingTest.class.php index bd923ca782..58af974831 100644 --- a/src/test/php/net/xp_framework/unittest/annotations/AnnotationParsingTest.class.php +++ b/src/test/php/net/xp_framework/unittest/annotations/AnnotationParsingTest.class.php @@ -234,7 +234,7 @@ public function multi_line_annotation() { $this->parse(" #[@interceptors(classes= array( 'net.xp_framework.unittest.core.FirstInterceptor', - 'net.xp_framework.unittest.core.SecondInterceptor', + 'net.xp_framework.unittest.core.SecondInterceptor' ))] ") ); diff --git a/src/test/php/net/xp_framework/unittest/annotations/BrokenAnnotationTest.class.php b/src/test/php/net/xp_framework/unittest/annotations/BrokenAnnotationTest.class.php index a339a627f1..3bdea1074c 100644 --- a/src/test/php/net/xp_framework/unittest/annotations/BrokenAnnotationTest.class.php +++ b/src/test/php/net/xp_framework/unittest/annotations/BrokenAnnotationTest.class.php @@ -2,6 +2,7 @@ use lang\XPClass; use lang\reflect\ClassParser; +use lang\ClassFormatException; /** * Tests the XP Framework's annotations @@ -22,152 +23,152 @@ protected function parse($input) { return (new ClassParser())->parseAnnotations($input, $this->getClassName()); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Unterminated annotation/')] + #[@test, @expect(ClassFormatException::class)] public function no_ending_bracket() { XPClass::forName('net.xp_framework.unittest.annotations.NoEndingBracket')->getAnnotations(); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Parse error/')] + #[@test, @expect(ClassFormatException::class)] public function missing_ending_bracket_in_key_value_pairs() { $this->parse("#[@attribute(key= 'value']"); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Parse error/')] + #[@test, @expect(ClassFormatException::class)] public function unterminated_single_quoted_string_literal() { $this->parse("#[@attribute(key= 'value)]"); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Parse error/')] + #[@test, @expect(ClassFormatException::class)] public function unterminated_double_quoted_string_literal() { $this->parse('#[@attribute(key= "value)]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Expecting "@"/')] + #[@test, @expect(ClassFormatException::class)] public function missing_annotation_after_comma_and_value() { $this->parse('#[@ignore("Test"), ]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Expecting "@"/')] + #[@test, @expect(ClassFormatException::class)] public function missing_annotation_after_comma() { $this->parse('#[@ignore, ]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Expecting "@"/')] + #[@test, @expect(ClassFormatException::class)] public function missing_annotation_after_second_comma() { $this->parse('#[@ignore, @test, ]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Parse error: Unterminated string/')] + #[@test, @expect(ClassFormatException::class)] public function unterminated_dq_string() { $this->parse('#[@ignore("Test)]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Parse error: Unterminated string/')] + #[@test, @expect(ClassFormatException::class)] public function unterminated_sq_string() { $this->parse("#[@ignore('Test)]"); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Malformed array/')] + #[@test, @expect(ClassFormatException::class)] public function unterminated_array() { $this->parse('#[@ignore(array(1]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Malformed array/')] + #[@test, @expect(ClassFormatException::class)] public function unterminated_array_key() { $this->parse('#[@ignore(name = array(1]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Malformed array/')] + #[@test, @expect(ClassFormatException::class)] public function malformed_array() { $this->parse('#[@ignore(array(1 ,, 2))]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Malformed array/')] + #[@test, @expect(ClassFormatException::class)] public function malformed_array_inside_key_value_pairs() { $this->parse('#[@ignore(name= array(1 ,, 2))]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Malformed array/')] + #[@test, @expect(ClassFormatException::class)] public function malformed_array_no_commas() { $this->parse('#[@ignore(array(1 2))]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Parse error: Expecting either "\(", "," or "\]"/')] + #[@test, @expect(ClassFormatException::class)] public function annotation_not_separated_by_commas() { $this->parse("#[@test @throws('rdbms.SQLConnectException')]"); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Parse error: Expecting either "\(", "," or "\]"/')] + #[@test, @expect(ClassFormatException::class)] public function too_many_closing_braces() { $this->parse("#[@throws('rdbms.SQLConnectException'))]"); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Undefined constant "editor"/')] + #[@test, @expect(ClassFormatException::class)] public function undefined_constant() { $this->parse('#[@$editorId: param(editor)]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/No such constant "EDITOR" in class/')] + #[@test, @expect(ClassFormatException::class)] public function undefined_class_constant() { $this->parse('#[@$editorId: param(self::EDITOR)]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Class ".+" could not be found/')] + #[@test, @expect(ClassFormatException::class)] public function undefined_class_in_new() { $this->parse('#[@$editorId: param(new NonExistantClass())]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Class ".+" could not be found/')] + #[@test, @expect(ClassFormatException::class)] public function undefined_class_in_constant() { $this->parse('#[@$editorId: param(NonExistantClass::CONSTANT)]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/No such field "EDITOR" in class/')] + #[@test, @expect(ClassFormatException::class)] public function undefined_class_member() { $this->parse('#[@$editorId: param(self::$EDITOR)]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Cannot access protected static field .+AnnotationParsingTest::\$hidden/')] + #[@test, @expect(ClassFormatException::class)] public function class_protected_static_member() { $this->parse('#[@value(AnnotationParsingTest::$hidden)]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Cannot access private static field .+AnnotationParsingTest::\$internal/')] + #[@test, @expect(ClassFormatException::class)] public function class_private_static_member() { $this->parse('#[@value(AnnotationParsingTest::$internal)]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/In `.+`: Syntax error/')] + #[@test, @expect(ClassFormatException::class)] public function function_without_braces() { $this->parse('#[@value(function)]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/In `.+`: Syntax error/')] + #[@test, @expect(ClassFormatException::class)] public function function_without_body() { $this->parse('#[@value(function())]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/In `.+`: Syntax error/')] + #[@test, @expect(ClassFormatException::class)] public function function_without_closing_curly() { $this->parse('#[@value(function() {)]'); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Parse error: Unexpected ","/')] + #[@test, @expect(ClassFormatException::class)] public function multi_value() { $this->parse("#[@xmlmapping('hw_server', 'server')]"); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Parse error: Unexpected ","/')] + #[@test, @expect(ClassFormatException::class)] public function multi_value_without_whitespace() { $this->parse("#[@xmlmapping('hw_server','server')]"); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Parse error: Unexpected ","/')] + #[@test, @expect(ClassFormatException::class)] public function multi_value_with_variable_types_backwards_compatibility() { $this->parse("#[@xmlmapping('hw_server', TRUE)]"); } - #[@test, @expect(class= 'lang.ClassFormatException', withMessage= '/Parse error: Unexpected ","/')] + #[@test, @expect(ClassFormatException::class)] public function parsingContinuesAfterMultiValue() { $this->parse("#[@xmlmapping('hw_server', 'server'), @restricted]"); } From 34efde94b6e2a12175af3bb836609e3046bebc64 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Feb 2015 22:04:00 +0100 Subject: [PATCH 15/23] Remove trailing comma --- .../net/xp_framework/unittest/util/cmd/ConsoleTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/net/xp_framework/unittest/util/cmd/ConsoleTest.class.php b/src/test/php/net/xp_framework/unittest/util/cmd/ConsoleTest.class.php index bde3ed533a..5b59413826 100644 --- a/src/test/php/net/xp_framework/unittest/util/cmd/ConsoleTest.class.php +++ b/src/test/php/net/xp_framework/unittest/util/cmd/ConsoleTest.class.php @@ -46,7 +46,7 @@ public function read() { #[@test, @values([ # "Hello\nHallo", # "Hello\rHallo", - # "Hello\r\nHallo", + # "Hello\r\nHallo" #])] public function readLine($variation) { Console::$in->setStream(new MemoryInputStream($variation)); From 00d94d042dc78ff26acf755031f3a07c4447b370 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 23 Feb 2015 00:44:46 +0100 Subject: [PATCH 16/23] Merge implementations of high-performing OneOf and flexible EitherOf Before: 1.887 seconds After: 1.517 seconds (running core.ini tests) --- .../lang/codedom/AnnotationSyntax.class.php | 38 ++++++++++-------- src/main/php/lang/codedom/AnyOf.class.php | 39 +++++++++++++++++++ src/main/php/lang/codedom/EitherOf.class.php | 29 -------------- src/main/php/lang/codedom/OneOf.class.php | 32 --------------- src/main/php/lang/codedom/PhpSyntax.class.php | 16 ++++---- 5 files changed, 68 insertions(+), 86 deletions(-) create mode 100755 src/main/php/lang/codedom/AnyOf.class.php delete mode 100755 src/main/php/lang/codedom/EitherOf.class.php delete mode 100755 src/main/php/lang/codedom/OneOf.class.php diff --git a/src/main/php/lang/codedom/AnnotationSyntax.class.php b/src/main/php/lang/codedom/AnnotationSyntax.class.php index 27589e6970..2d3d491e92 100755 --- a/src/main/php/lang/codedom/AnnotationSyntax.class.php +++ b/src/main/php/lang/codedom/AnnotationSyntax.class.php @@ -3,11 +3,11 @@ class AnnotationSyntax extends Syntax { static function __static() { - $number= new OneOf([ + $number= new AnyOf([ T_DNUMBER => new Sequence([], function($values) { return (double)$values[0]; }), T_LNUMBER => new Sequence([], function($values) { return (int)$values[0]; }) ]); - $key= new OneOf([ + $key= new AnyOf([ T_STRING => new Sequence([], function($values) { return $values[0]; }), T_RETURN => new Returns('return'), T_CLASS => new Returns('class'), @@ -31,31 +31,35 @@ function($values) { return substr($values[0], 1); } [new Token('('), new Rule(':expr'), new Token(')')], function($values) { return $values[1]; } )), - ':expr' => new EitherOf([ - new Sequence([$number], function($values) { - return new AnnotationValue($values[0]); - }), - new Sequence([new Token('-'), $number], function($values) { + ':expr' => new AnyOf([ + '-' => new Sequence([$number], function($values) { return new AnnotationValue(-1 * $values[1]); }), - new Sequence([new Token('+'), $number], function($values) { + '+' => new Sequence([$number], function($values) { return new AnnotationValue($values[1]); }), - new Sequence([new Token(T_CONSTANT_ENCAPSED_STRING)], function($values) { - return new AnnotationValue(eval('return '.$values[0].';')); - }), - new Sequence([new Token('['), new Optional(new ListOf(new Rule(':element'))), new Token(']')], function($values) { + '[' => new Sequence([new Optional(new ListOf(new Rule(':element'))), new Token(']')], function($values) { return new AnnotationArray((array)$values[1]); }), - new Sequence([new Token(T_ARRAY), new Token('('), new Optional(new ListOf(new Rule(':element'))), new Token(')')], function($values) { + T_ARRAY => new Sequence([new Token('('), new Optional(new ListOf(new Rule(':element'))), new Token(')')], function($values) { return new AnnotationArray((array)$values[2]); }), - new Sequence([new Token(T_NEW), $type, new Token('('), new Optional(new ListOf(new Rule(':expr'))), new Token(')')], function($values) { + T_DNUMBER => new Sequence([], function($values) { + return new AnnotationValue((double)$values[0]); + }), + T_LNUMBER => new Sequence([], function($values) { + return new AnnotationValue((int)$values[0]); + }), + T_CONSTANT_ENCAPSED_STRING => new Sequence([], function($values) { + return new AnnotationValue(eval('return '.$values[0].';')); + }), + T_NEW => new Sequence([$type, new Token('('), new Optional(new ListOf(new Rule(':expr'))), new Token(')')], function($values) { return new AnnotationInstance(implode('', $values[1]), (array)$values[3]); }), - new Sequence([new Token(T_FUNCTION), new Token('('), new SkipOver('(', ')'), new Token('{'), new SkipOver('{', '}')], function($values) { + T_FUNCTION => new Sequence([new Token('('), new SkipOver('(', ')'), new Token('{'), new SkipOver('{', '}')], function($values) { return new AnnotationClosure($values[2], $values[4]); }), + ], [ new Sequence([$type, new Token(T_DOUBLE_COLON), new Rule(':member')], function($values) { return new AnnotationMember(implode('', $values[0]), $values[2]); }), @@ -69,7 +73,7 @@ function($values) { return $values[1]; } ':pair' => new Sequence([$key, new Token('='), new Rule(':expr')], function($values) { return [$values[0] => $values[2]]; }), - ':element' => new EitherOf([ + ':element' => new AnyOf([], [ new Sequence([new Token(T_CONSTANT_ENCAPSED_STRING), new Token(T_DOUBLE_ARROW), new Rule(':expr')], function($values) { return [trim($values[0], '"\'') => $values[2]]; }), @@ -78,7 +82,7 @@ function($values) { return $values[1]; } }) ]), ':member' => new Sequence( - [new OneOf([ + [new AnyOf([ T_STRING => new Sequence([], function($values) { return $values[0]; }), T_CLASS => new Sequence([], function($values) { return $values[0]; }), T_VARIABLE => new Sequence([], function($values) { return $values[0]; }), diff --git a/src/main/php/lang/codedom/AnyOf.class.php b/src/main/php/lang/codedom/AnyOf.class.php new file mode 100755 index 0000000000..1a8fe019cd --- /dev/null +++ b/src/main/php/lang/codedom/AnyOf.class.php @@ -0,0 +1,39 @@ +detect= $detect; + $this->try= $try; + } + + private static function tokenName($token) { + return is_int($token) ? token_name($token) : '`'.$token.'`'; + } + + public function consume($rules, $stream, $values) { + $case= $stream->token(); + if (isset($this->detect[$case[0]])) { + $stream->next(); + return $this->detect[$case[0]]->consume($rules, $stream, [is_array($case) ? $case[1] : $case]); + } else { + $begin= $stream->position(); + foreach ($this->try as $try) { + $result= $try->consume($rules, $stream, []); + if ($result->matched()) return $result; + $stream->reset($begin); + } + } + + return new Unexpected( + sprintf( + 'Unexpected %s, expecting one of %s or %s', + is_array($case) ? token_name($case[0]) : $case, + implode(', ', array_map(['self', 'tokenName'], array_keys($this->detect))), + implode(', ', array_map(['xp', 'stringOf'], array_keys($this->try))) + ), + $stream->line() + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/EitherOf.class.php b/src/main/php/lang/codedom/EitherOf.class.php deleted file mode 100755 index 1702cfce2a..0000000000 --- a/src/main/php/lang/codedom/EitherOf.class.php +++ /dev/null @@ -1,29 +0,0 @@ -cases= $cases; - } - - public function consume($rules, $stream, $values) { - $begin= $stream->position(); - foreach ($this->cases as $case) { - $result= $case->consume($rules, $stream, []); - if ($result->matched()) return $result; - $stream->reset($begin); - } - - $token= $stream->token(); - return new Unexpected( - sprintf( - 'Unexpected %s, expecting one of %s', - is_array($token) ? token_name($token[0]) : $token, - \xp::stringOf($this->cases) - ), - $stream->line() - ); - } -} \ No newline at end of file diff --git a/src/main/php/lang/codedom/OneOf.class.php b/src/main/php/lang/codedom/OneOf.class.php deleted file mode 100755 index 4a462b7414..0000000000 --- a/src/main/php/lang/codedom/OneOf.class.php +++ /dev/null @@ -1,32 +0,0 @@ -cases= $cases; - } - - private function tokenName($token) { - return is_int($token) ? token_name($token) : '`'.$token.'`'; - } - - public function consume($rules, $stream, $values) { - $case= $stream->token(); - if (isset($this->cases[$case[0]])) { - $stream->next(); - return $this->cases[$case[0]]->consume($rules, $stream, [is_array($case) ? $case[1] : $case]); - } - - return new Unexpected( - sprintf( - 'Unexpected %s, expecting one of %s', - is_array($case) ? token_name($case[0]) : $case, - implode(', ', array_map([$this, 'tokenName'], array_keys($this->cases))) - ), - $stream->line() - ); - } -} \ No newline at end of file diff --git a/src/main/php/lang/codedom/PhpSyntax.class.php b/src/main/php/lang/codedom/PhpSyntax.class.php index 8710565eee..66a599b275 100755 --- a/src/main/php/lang/codedom/PhpSyntax.class.php +++ b/src/main/php/lang/codedom/PhpSyntax.class.php @@ -14,7 +14,7 @@ static function __static() { } return new CodeUnit($values[1], $imports, $values[4]); }), - ':namespace' => new Optional(new OneOf([ + ':namespace' => new Optional(new AnyOf([ T_VARIABLE => new Sequence([new Token('='), new Token(T_CONSTANT_ENCAPSED_STRING), new Token(';')], function($values) { return substr($values[2], 1, -1); }), @@ -22,7 +22,7 @@ static function __static() { return strtr(implode('', $values[1]), '\\', '.'); }) ])), - ':imports' => new Repeated(new OneOf([ + ':imports' => new Repeated(new AnyOf([ T_USE => new Sequence([$type, new Token(';')], function($values) { return strtr(implode('', $values[1]), '\\', '.'); }), @@ -30,7 +30,7 @@ static function __static() { return trim($values[3], '\'"'); }) ])), - ':uses_opt' => new Repeated(new OneOf([ + ':uses_opt' => new Repeated(new AnyOf([ T_STRING => new Sequence([new Token('('), new SkipOver('(', ')'), new Token(';')], function($values) { if ('uses' === $values[0]) { return array_map(function($class) { return trim($class, "'\" "); }, explode(',', $values[2])); @@ -43,7 +43,7 @@ static function __static() { [ new Rule(':annotations'), $modifiers, - new OneOf([ + new AnyOf([ T_CLASS => new Sequence([new Token(T_STRING), new Rule(':class_parent'), new Rule(':class_implements'), new Rule(':type_body')], function($values) { return new ClassDeclaration(0, null, $values[1], $values[2], (array)$values[3], $values[4]); }), @@ -86,7 +86,7 @@ function($values) { } return new TypeBody($body['member'], $body['trait']); }), - ':member' => new EitherOf([ + ':member' => new AnyOf([], [ new Sequence([new Token(T_USE), $type, new Token(';')], function($values) { return [new TraitUsage(implode('', $values[1]))]; }), @@ -98,7 +98,7 @@ function($values) { new Rule(':annotations'), $modifiers, new Rule(':annotations'), // Old way of annotating fields, in combination with grouped syntax - new OneOf([ + new AnyOf([ T_FUNCTION => new Sequence([new Token(T_STRING), new Token('('), new SkipOver('(', ')'), new Rule(':method')], function($values, $stream) { $details= self::details($stream->comment()); if ('__construct' === $values[1]) { @@ -126,11 +126,11 @@ function($values) { ':field' => new Sequence( [ new Optional(new Sequence([new Token('='), new Expr()], function($values) { return $values[1]; })), - new OneOf([';' => new Returns(null), ',' => new Returns(null)]) + new AnyOf([';' => new Returns(null), ',' => new Returns(null)]) ], function($values) { return $values[0]; } ), - ':method' => new OneOf([ + ':method' => new AnyOf([ ';' => new Returns(null), '{' => new Sequence([new SkipOver('{', '}')], function($values) { return $values[1]; }) ]) From d09cc97596410a4012962407f7951e409986d5b6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 23 Feb 2015 00:49:06 +0100 Subject: [PATCH 17/23] QA: Reflow code --- src/main/php/lang/codedom/PhpSyntax.class.php | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/main/php/lang/codedom/PhpSyntax.class.php b/src/main/php/lang/codedom/PhpSyntax.class.php index 66a599b275..e785a68646 100755 --- a/src/main/php/lang/codedom/PhpSyntax.class.php +++ b/src/main/php/lang/codedom/PhpSyntax.class.php @@ -64,19 +64,15 @@ function($values) { ':annotations' => new Optional(new Sequence([new Token(600)], function($values) { return $values[0]; })), - ':class_parent' => new Optional( - new Sequence([new Token(T_EXTENDS), $type], function($values) { return implode('', $values[1]); }) - ), - ':class_implements' => new Optional( - new Sequence([new Token(T_IMPLEMENTS), new ListOf($type)], function($values) { - return array_map(function($v) { return implode('', $v); }, $values[1]); - }) - ), - ':interface_parents' => new Optional( - new Sequence([new Token(T_EXTENDS), new ListOf($type)], function($values) { - return array_map(function($v) { return implode('', $v); }, $values[1]); - }) - ), + ':class_parent' => new Optional(new Sequence([new Token(T_EXTENDS), $type], function($values) { + return implode('', $values[1]); + })), + ':class_implements' => new Optional(new Sequence([new Token(T_IMPLEMENTS), new ListOf($type)], function($values) { + return array_map(function($v) { return implode('', $v); }, $values[1]); + })), + ':interface_parents' => new Optional(new Sequence([new Token(T_EXTENDS), new ListOf($type)], function($values) { + return array_map(function($v) { return implode('', $v); }, $values[1]); + })), ':type_body' => new Sequence([new Token('{'), new Repeated(new Rule(':member')), new Token('}')], function($values) { $body= ['member' => [], 'trait' => []]; foreach ($values[1] as $decl) { @@ -86,13 +82,14 @@ function($values) { } return new TypeBody($body['member'], $body['trait']); }), - ':member' => new AnyOf([], [ - new Sequence([new Token(T_USE), $type, new Token(';')], function($values) { + ':member' => new AnyOf([ + T_USE => new Sequence([$type, new Token(';')], function($values) { return [new TraitUsage(implode('', $values[1]))]; }), - new Sequence([new Token(T_CONST), new ListOf(new Rule(':const')), new Token(';')], function($values) { + T_CONST => new Sequence([new ListOf(new Rule(':const')), new Token(';')], function($values) { return $values[1]; }), + ], [ new Sequence( [ new Rule(':annotations'), From 1220206d219ba7f6ba85925e2096cdf72f5edc39 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 24 Feb 2015 22:36:27 +0100 Subject: [PATCH 18/23] Add isTrait(), isInterface(), isClass() --- src/main/php/lang/codedom/ClassDeclaration.class.php | 3 +++ .../php/lang/codedom/InterfaceDeclaration.class.php | 3 +++ src/main/php/lang/codedom/TraitDeclaration.class.php | 3 +++ src/main/php/lang/codedom/TypeDeclaration.class.php | 10 ++++++++++ 4 files changed, 19 insertions(+) diff --git a/src/main/php/lang/codedom/ClassDeclaration.class.php b/src/main/php/lang/codedom/ClassDeclaration.class.php index 05693ec748..0819d9677b 100755 --- a/src/main/php/lang/codedom/ClassDeclaration.class.php +++ b/src/main/php/lang/codedom/ClassDeclaration.class.php @@ -12,6 +12,9 @@ public function __construct($modifiers, $annotations, $name, $extends, $implemen $this->implements= $implements; } + /** @return bool */ + public function isClass() { return true; } + /** * Creates a string representation * diff --git a/src/main/php/lang/codedom/InterfaceDeclaration.class.php b/src/main/php/lang/codedom/InterfaceDeclaration.class.php index 21d09dceaa..0af5c195b8 100755 --- a/src/main/php/lang/codedom/InterfaceDeclaration.class.php +++ b/src/main/php/lang/codedom/InterfaceDeclaration.class.php @@ -11,6 +11,9 @@ public function __construct($modifiers, $annotations, $name, $parents, $body) { $this->parents= $parents; } + /** @return bool */ + public function isInterface() { return true; } + /** * Creates a string representation * diff --git a/src/main/php/lang/codedom/TraitDeclaration.class.php b/src/main/php/lang/codedom/TraitDeclaration.class.php index 43323a735b..0c784acdac 100755 --- a/src/main/php/lang/codedom/TraitDeclaration.class.php +++ b/src/main/php/lang/codedom/TraitDeclaration.class.php @@ -9,6 +9,9 @@ public function __construct($modifiers, $annotations, $name, $body) { parent::__construct($modifiers, $annotations, $name, $body); } + /** @return bool */ + public function isTrait() { return true; } + /** * Creates a string representation * diff --git a/src/main/php/lang/codedom/TypeDeclaration.class.php b/src/main/php/lang/codedom/TypeDeclaration.class.php index ae4c057503..0189341275 100755 --- a/src/main/php/lang/codedom/TypeDeclaration.class.php +++ b/src/main/php/lang/codedom/TypeDeclaration.class.php @@ -23,4 +23,14 @@ public function name() { return $this->name; } /** @return lang.codedom.TypeBody */ public function body() { return $this->body; } + + /** @return bool */ + public function isClass() { return false; } + + /** @return bool */ + public function isInterface() { return false; } + + /** @return bool */ + public function isTrait() { return false; } + } \ No newline at end of file From 5f3f44fa0485a1a5270493c941acbef8f97d0ed9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 24 Feb 2015 22:36:46 +0100 Subject: [PATCH 19/23] Add body() accessor --- src/main/php/lang/codedom/ConstructorDeclaration.class.php | 3 +++ src/main/php/lang/codedom/MethodDeclaration.class.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/main/php/lang/codedom/ConstructorDeclaration.class.php b/src/main/php/lang/codedom/ConstructorDeclaration.class.php index eaf0e40eab..9ff80f34b7 100755 --- a/src/main/php/lang/codedom/ConstructorDeclaration.class.php +++ b/src/main/php/lang/codedom/ConstructorDeclaration.class.php @@ -34,6 +34,9 @@ public function returns() { return null; } /** @return string[] */ public function throws() { return $this->throws; } + /** @return string */ + public function body() { return $this->body; } + /** * Creates a string representation * diff --git a/src/main/php/lang/codedom/MethodDeclaration.class.php b/src/main/php/lang/codedom/MethodDeclaration.class.php index 66fad12b67..78fe38bd8d 100755 --- a/src/main/php/lang/codedom/MethodDeclaration.class.php +++ b/src/main/php/lang/codedom/MethodDeclaration.class.php @@ -37,6 +37,9 @@ public function returns() { return $this->returns; } /** @return string[] */ public function throws() { return $this->throws; } + /** @return string */ + public function body() { return $this->body; } + /** * Creates a string representation * From 5ccb5ec7ac4e69bf609bf0088fdc58555cefdc3e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 24 Feb 2015 23:30:09 +0100 Subject: [PATCH 20/23] Method / Constructor declaration: arguments^Wparameters --- .../php/lang/codedom/ClassDeclaration.class.php | 6 ++++++ .../lang/codedom/ConstructorDeclaration.class.php | 14 +++++++------- .../php/lang/codedom/MethodDeclaration.class.php | 14 +++++++------- src/main/php/lang/reflect/ClassParser.class.php | 2 +- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/main/php/lang/codedom/ClassDeclaration.class.php b/src/main/php/lang/codedom/ClassDeclaration.class.php index 0819d9677b..adb15b5ef7 100755 --- a/src/main/php/lang/codedom/ClassDeclaration.class.php +++ b/src/main/php/lang/codedom/ClassDeclaration.class.php @@ -15,6 +15,12 @@ public function __construct($modifiers, $annotations, $name, $extends, $implemen /** @return bool */ public function isClass() { return true; } + /** @return string */ + public function parent() { return $this->extends; } + + /** @return string[] */ + public function interfaces() { return $this->implements; } + /** * Creates a string representation * diff --git a/src/main/php/lang/codedom/ConstructorDeclaration.class.php b/src/main/php/lang/codedom/ConstructorDeclaration.class.php index 9ff80f34b7..713ad7f073 100755 --- a/src/main/php/lang/codedom/ConstructorDeclaration.class.php +++ b/src/main/php/lang/codedom/ConstructorDeclaration.class.php @@ -4,7 +4,7 @@ use lang\reflect\Modifiers; class ConstructorDeclaration extends MemberDeclaration { - private $arguments, $body, $throws; + private $parameters, $body, $throws; /** * Creates a new method declaration @@ -12,12 +12,12 @@ class ConstructorDeclaration extends MemberDeclaration { * @param int $modifiers * @param string $annotations * @param string $name - * @param string[] $arguments Argument types + * @param string[] $parameters Argument types * @param string $body Code in body as string */ - public function __construct($modifiers, $annotations, $name, $arguments, $throws, $body) { + public function __construct($modifiers, $annotations, $name, $parameters, $throws, $body) { parent::__construct($modifiers, $annotations, $name); - $this->arguments= $arguments; + $this->parameters= $parameters; $this->throws= $throws; $this->body= $body; } @@ -26,7 +26,7 @@ public function __construct($modifiers, $annotations, $name, $arguments, $throws public function isMethod() { return true; } /** @return string[] */ - public function arguments() { return $this->arguments; } + public function parameters() { return $this->parameters; } /** @return string */ public function returns() { return null; } @@ -49,7 +49,7 @@ public function toString() { $this->annotations ? $this->annotations.' ' : '', implode(' ', Modifiers::namesOf($this->modifiers)), $this->name, - implode(', ', $this->arguments), + implode(', ', $this->parameters), $this->throws ? ' throws '.implode(' ', $this->throws) : '', $this->body ? ' { '.strlen($this->body).' bytes }' : '' ); @@ -66,7 +66,7 @@ public function equals($cmp) { $this->modifiers === $cmp->modifiers && $this->annotations === $cmp->annotations && $this->name === $cmp->name && - Objects::equal($this->arguments, $cmp->arguments) && + Objects::equal($this->parameters, $cmp->parameters) && Objects::equal($this->throws, $cmp->throws) && Objects::equal($this->body, $cmp->body) ); diff --git a/src/main/php/lang/codedom/MethodDeclaration.class.php b/src/main/php/lang/codedom/MethodDeclaration.class.php index 78fe38bd8d..61eda527b7 100755 --- a/src/main/php/lang/codedom/MethodDeclaration.class.php +++ b/src/main/php/lang/codedom/MethodDeclaration.class.php @@ -4,7 +4,7 @@ use lang\reflect\Modifiers; class MethodDeclaration extends MemberDeclaration { - private $arguments, $returns, $throws, $body; + private $parameters, $returns, $throws, $body; /** * Creates a new method declaration @@ -12,14 +12,14 @@ class MethodDeclaration extends MemberDeclaration { * @param int $modifiers * @param string $annotations * @param string $name - * @param string[] $arguments Argument types + * @param string[] $parameters Argument types * @param string $returns Return type * @param string[] $throws Exception types * @param string $body Code in body as string */ - public function __construct($modifiers, $annotations, $name, $arguments, $returns, $throws, $body) { + public function __construct($modifiers, $annotations, $name, $parameters, $returns, $throws, $body) { parent::__construct($modifiers, $annotations, $name); - $this->arguments= $arguments; + $this->parameters= $parameters; $this->returns= $returns; $this->throws= $throws; $this->body= $body; @@ -29,7 +29,7 @@ public function __construct($modifiers, $annotations, $name, $arguments, $return public function isMethod() { return true; } /** @return string[] */ - public function arguments() { return $this->arguments; } + public function parameters() { return $this->parameters; } /** @return string */ public function returns() { return $this->returns; } @@ -52,7 +52,7 @@ public function toString() { $this->annotations ? $this->annotations.' ' : '', implode(' ', Modifiers::namesOf($this->modifiers)), $this->name, - implode(', ', $this->arguments), + implode(', ', $this->parameters), $this->returns, $this->throws ? ' throws '.implode(' ', $this->throws) : '', $this->body ? ' { '.strlen($this->body).' bytes }' : '' @@ -71,7 +71,7 @@ public function equals($cmp) { $this->annotations === $cmp->annotations && $this->name === $cmp->name && $this->returns === $cmp->returns && - Objects::equal($this->arguments, $cmp->arguments) && + Objects::equal($this->parameters, $cmp->parameters) && Objects::equal($this->throws, $cmp->throws) && Objects::equal($this->body, $cmp->body) ); diff --git a/src/main/php/lang/reflect/ClassParser.class.php b/src/main/php/lang/reflect/ClassParser.class.php index bd7bd41d3b..c1fa21fd91 100755 --- a/src/main/php/lang/reflect/ClassParser.class.php +++ b/src/main/php/lang/reflect/ClassParser.class.php @@ -66,7 +66,7 @@ public function parseDetails($bytes, $context= '') { $details[0][$member->name()]= [DETAIL_ANNOTATIONS => $annotations[0]]; } else if ($member->isMethod()) { $details[1][$member->name()]= [ - DETAIL_ARGUMENTS => $member->arguments(), + DETAIL_ARGUMENTS => $member->parameters(), DETAIL_RETURNS => $member->returns(), DETAIL_THROWS => $member->throws(), DETAIL_ANNOTATIONS => $annotations[0], From 8ba4326758c55d0d99cefbf8977ea07c33ca88c3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 26 Feb 2015 19:45:09 +0100 Subject: [PATCH 21/23] Refactor: Migrate generic types creation to CodeDOM API --- src/main/php/lang/GenericTypes.class.php | 408 +++++++++--------- .../codedom/ConstantDeclaration.class.php | 15 +- src/main/php/lang/codedom/Expr.class.php | 2 +- .../lang/codedom/FieldDeclaration.class.php | 3 + .../lang/codedom/MethodDeclaration.class.php | 4 +- src/main/php/lang/codedom/Parameter.class.php | 69 +++ src/main/php/lang/codedom/PhpSyntax.class.php | 26 +- src/main/php/lang/codedom/Stream.class.php | 2 +- .../php/lang/reflect/ClassParser.class.php | 2 +- .../unittest/core/PhpSyntaxTest.class.php | 14 +- .../reflection/ClassDetailsTest.class.php | 2 +- 11 files changed, 319 insertions(+), 228 deletions(-) create mode 100755 src/main/php/lang/codedom/Parameter.class.php diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index e84dbc4452..5e52c22e51 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -1,5 +1,7 @@ name]; + $meta['class'][DETAIL_GENERIC]= [$base->name, $arguments]; // Parse placeholders into a lookup map $placeholders= []; @@ -72,232 +75,66 @@ public function newType0($base, $arguments) { throw new IllegalStateException($base->name); } - // Namespaced class + $codeunit= (new \lang\codedom\PhpSyntax())->parse($bytes); + if (false !== ($ns= strrpos($name, '\\'))) { $decl= substr($name, $ns + 1); $namespace= substr($name, 0, $ns); - $src= 'namespace '.$namespace.';'; + $source= 'namespace '.$namespace.';'; } else { $decl= $name; $namespace= null; - $src= ''; + $source= ''; } - // Replace source - $annotation= null; $imports= []; - $matches= []; - $state= [0]; - $counter= 0; - $initialize= false; - $tokens= token_get_all($bytes); - for ($i= 0, $s= sizeof($tokens); $i < $s; $i++) { - if (T_COMMENT === $tokens[$i][0]) { - continue; - } else if (0 === $state[0]) { - if (T_ABSTRACT === $tokens[$i][0] || T_FINAL === $tokens[$i][0]) { - $src.= $tokens[$i][1].' '; - } else if (T_CLASS === $tokens[$i][0] || T_INTERFACE === $tokens[$i][0]) { - $meta['class'][DETAIL_GENERIC]= [$base->name, $arguments]; - $src.= $tokens[$i][1].' '.$decl; - array_unshift($state, $tokens[$i][0]); - } else if (T_USE === $tokens[$i][0]) { - $i+= 2; - $use= ''; - while ((T_STRING === $tokens[$i][0] || T_NS_SEPARATOR === $tokens[$i][0]) && $i < $s) { - $use.= $tokens[$i][1]; - $i++; - } - $imports[substr($use, strrpos($use, '\\')+ 1)]= $use; - $src.= 'use '.$use.';'; - } - continue; - } else if (T_CLASS === $state[0]) { - if (T_EXTENDS === $tokens[$i][0]) { - $i+= 2; - $parent= ''; - while ((T_STRING === $tokens[$i][0] || T_NS_SEPARATOR === $tokens[$i][0]) && $i < $s) { - $parent.= $tokens[$i][1]; - $i++; - } - $i--; - if (isset($annotations['generic']['parent'])) { - $xargs= []; - foreach (explode(',', $annotations['generic']['parent']) as $j => $placeholder) { - $xargs[]= Type::forName(strtr(ltrim($placeholder), $placeholders)); + foreach ($codeunit->imports() as $type) { + $imports[substr($type, strrpos($type, '.')+ 1)]= $type; + $source.= 'use '.strtr($type, '.', '\\').";\n"; + } + + $declaration= $codeunit->declaration(); + $generic= $this->genericAnnotation($declaration->annotations(), $base, $imports); + if ($declaration->isInterface()) { + $source.= $this->interfaceDeclaration($decl, $base, $declaration, $generic, $placeholders); + } else if ($declaration->isClass()) { + $source.= $this->classDeclaration($decl, $base, $declaration, $generic, $placeholders); + } else { + throw new IllegalStateException('Unsupported generic type '.$declaration->toString()); + } + + foreach ($declaration->body()->members() as $member) { + if ($member->isMethod()) { + $annotation= $this->genericAnnotation($member->annotations(), $base, $imports); + + $generic= []; + if (isset($annotation['params'])) { + foreach (explode(',', $annotation['params']) as $i => $placeholder) { + if ('' === ($replaced= strtr(ltrim($placeholder), $placeholders))) { + $generic[$i]= null; + } else { + $meta[1][$member->name()][DETAIL_ARGUMENTS][$i]= $replaced; + $generic[$i]= $replaced; } - $src.= ' extends \\'.$this->newType0($base->getParentClass(), $xargs); - } else { - $src.= ' extends '.$parent; - } - } else if (T_IMPLEMENTS === $tokens[$i][0]) { - $src.= ' implements'; - $counter= 0; - $annotation= @$annotations['generic']['implements']; - array_unshift($state, T_CLASS); - array_unshift($state, 5); - } else if ('{' === $tokens[$i][0]) { - array_shift($state); - array_unshift($state, 1); - $src.= ' { public static $__generic= [];'; - $initialize= true; - } - continue; - } else if (T_INTERFACE === $state[0]) { - if (T_EXTENDS === $tokens[$i][0]) { - $src.= ' extends'; - $counter= 0; - $annotation= @$annotations['generic']['extends']; - array_unshift($state, T_INTERFACE); - array_unshift($state, 5); - } else if ('{' === $tokens[$i][0]) { - array_shift($state); - array_unshift($state, 1); - $src.= ' {'; - } - continue; - } else if (1 === $state[0]) { // Class body - if (T_FUNCTION === $tokens[$i][0]) { - $braces= 0; - $parameters= $default= []; - array_unshift($state, 3); - array_unshift($state, 2); - $m= $tokens[$i+ 2][1]; - $p= 0; - $annotations= [$meta[1][$m][DETAIL_ANNOTATIONS], $meta[1][$m][DETAIL_TARGET_ANNO]]; - } else if ('}' === $tokens[$i][0]) { - $src.= '}'; - break; - } else if (T_CLOSE_TAG === $tokens[$i][0]) { - break; - } - } else if (2 === $state[0]) { // Method declaration - if ('(' === $tokens[$i][0]) { - $braces++; - } else if (')' === $tokens[$i][0]) { - $braces--; - if (0 === $braces) { - array_shift($state); - $src.= ')'; - continue; } } - if (T_VARIABLE === $tokens[$i][0]) { - $parameters[]= $tokens[$i][1]; - } else if (',' === $tokens[$i][0]) { - // Skip - } else if ('=' === $tokens[$i][0]) { - $p= sizeof($parameters)- 1; - $default[$p]= ''; - } else if (T_WHITESPACE !== $tokens[$i][0] && isset($default[$p])) { - $default[$p].= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; + if (isset($annotation['return'])) { + $meta[1][$member->name()][DETAIL_RETURNS]= strtr($annotation['return'], $placeholders); } - } 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 (isset($annotations[0]['generic']['params'])) { - foreach (explode(',', $annotations[0]['generic']['params']) as $j => $placeholder) { - if ('' !== ($replaced= strtr(ltrim($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); - } - if (isset($annotations[0]['generic']['params'])) { - $generic= []; - foreach (explode(',', $annotations[0]['generic']['params']) as $j => $placeholder) { - if ('' === ($replaced= strtr(ltrim($placeholder), $placeholders))) { - $generic[$j]= null; - } else { - $meta[1][$m][DETAIL_ARGUMENTS][$j]= $replaced; - $generic[$j]= $replaced; - } - } - foreach ($generic as $j => $type) { - if (null === $type) { - continue; - } else if ('...' === substr($type, -3)) { - $src.= $j ? '$�args= array_slice(func_get_args(), '.$j.');' : '$�args= func_get_args();'; - $src.= ( - ' if (!is(\''.substr($generic[$j], 0, -3).'[]\', $�args)) throw new \lang\IllegalArgumentException('. - '"Vararg '.($j + 1).' passed to ".__METHOD__."'. - ' must be of '.$type.', ".\xp::stringOf($�args)." given"'. - ');' - ); - } else { - $src.= ( - ' if ('.(isset($default[$j]) ? '('.$default[$j].' !== '.$parameters[$j].') && ' : ''). - '!is(\''.$generic[$j].'\', '.$parameters[$j].')) throw new \lang\IllegalArgumentException('. - '"Argument '.($j + 1).' passed to ".__METHOD__."'. - ' must be of '.$type.', ".\xp::typeOf('.$parameters[$j].')." given"'. - ');' - ); - } - } - } - $annotations= []; - unset($meta[1][$m][DETAIL_ANNOTATIONS]['generic']); - continue; - } - } else if (4 === $state[0]) { // Method body - if ('{' === $tokens[$i][0]) { - $braces++; - } else if ('}' === $tokens[$i][0]) { - $braces--; - if (0 === $braces) array_shift($state); - } else if (T_VARIABLE === $tokens[$i][0] && isset($placeholders[$v= substr($tokens[$i][1], 1)])) { - $src.= 'self::$__generic["'.$v.'"]'; - continue; - } - } else if (5 === $state[0]) { // Implements (class), Extends (interface) - if (T_STRING === $tokens[$i][0] || T_NS_SEPARATOR === $tokens[$i][0]) { - $rel= ''; - while ((T_STRING === $tokens[$i][0] || T_NS_SEPARATOR === $tokens[$i][0]) && $i < $s) { - $rel.= $tokens[$i][1]; - $i++; - } - $i--; - '\\' === $rel{0} || $rel= isset($imports[$rel]) ? $imports[$rel] : $namespace.'\\'.$rel; - if (isset($annotation[$counter])) { - $iargs= []; - foreach (explode(',', $annotation[$counter]) as $j => $placeholder) { - $iargs[]= Type::forName(strtr(ltrim($placeholder), $placeholders)); - } - $src.= '\\'.$this->newType0(new XPClass(new \ReflectionClass($rel)), $iargs); - } else { - $src.= $rel; - } - $counter++; - continue; - } else if ('{' === $tokens[$i][0]) { - array_shift($state); - array_shift($state); - $i--; - continue; - } + $source.= $this->methodDeclaration($member, $generic, $placeholders); + } else if ($member->isField()) { + $source.= $this->fieldDeclaration($member); + } else if ($member->isConstant()) { + $source.= $this->constantDeclaration($member); } - - $src.= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; } + $source.= '}'; // Create class // DEBUG fputs(STDERR, "@* ".substr($src, 0, strpos($src, '{'))." -> $qname\n"); - eval($src); - if ($initialize) { + eval($source); + if ($declaration->isClass()) { foreach ($components as $i => $component) { $name::$__generic[$component]= $arguments[$i]; } @@ -315,4 +152,161 @@ public function newType0($base, $arguments) { return $name; } + + private function genericAnnotation($annotations, $base, $imports) { + if (null === $annotations) { + return []; + } else { + $parsed= (new \lang\codedom\AnnotationSyntax())->parse($annotations)->resolve($base->getName(), $imports); + return isset($parsed[null]['generic']) ? $parsed[null]['generic'] : []; + } + } + + private function typeArgs($annotation, $placeholders) { + $args= []; + foreach (explode(',', $annotation) as $j => $placeholder) { + $args[]= Type::forName(strtr(ltrim($placeholder), $placeholders)); + } + return $args; + } + + /** + * Returns class declaration + */ + private function classDeclaration($name, $base, $declaration, $generic, $placeholders) { + if (isset($generic['parent'])) { + $parent= $this->newType0($base->getParentClass(), $this->typeArgs($generic['parent'], $placeholders)); + } else { + $parent= $base->getParentClass()->literal(); + } + + $implements= []; + $annotation= isset($generic['implements']) ? $generic['implements'] : []; + foreach ($base->getDeclaredInterfaces() as $num => $interface) { + if (isset($annotation[$num])) { + $implements[]= $this->newType0($interface, $this->typeArgs($annotation[$num], $placeholders)); + } else { + $implements[]= $interface->literal(); + } + } + + if (Modifiers::isAbstract($declaration->modifiers())) { + $modifiers= 'abstract '; + } else { + $modifiers= ''; + } + + return sprintf( + "%sclass %s extends \\%s%s {\n public static \$__generic= [];\n", + $modifiers, + $name, + $parent, + $implements ? ' implements \\'.implode(', \\', $implements) : '' + ); + } + + /** + * Returns interface declaration + */ + private function interfaceDeclaration($name, $base, $declaration, $generic, $placeholders) { + $extends= []; + $annotation= isset($generic['extends']) ? $generic['extends'] : []; + + foreach ($base->getDeclaredInterfaces() as $num => $parent) { + if (isset($annotation[$num])) { + $extends[]= $this->newType0($parent, $this->typeArgs($annotation[$num], $placeholders)); + } else { + $extends[]= $parent->literal(); + } + } + + return sprintf( + "interface %s%s{\n", + $name, + $extends ? ' extends \\'.implode(', \\', $extends) : '' + ); + } + + /** + * Returns method declaration + */ + private function methodDeclaration($declaration, $generic, $placeholders) { + $parameters= $declaration->parameters(); + + $signature= ''; + foreach ($declaration->parameters() as $i => $param) { + $signature.= sprintf( + ', %s $%s%s', + $param->restriction(), + $param->name(), + $param->hasDefault() ? '= '.$param->defaultValue() : '' + ); + } + + $checks= ''; + foreach ($generic as $i => $type) { + if (null === $type) { + continue; + } else if ('...' === substr($type, -3)) { + $checks= $i ? ' $�args= array_slice(func_get_args(), '.$j.');' : ' $�args= func_get_args();'; + $checks.= sprintf( + " is('%s[]', \$�args) || raise('lang.IllegalArgumentException', 'Vararg #%d passed to %s must be of %1\$s, '.\xp::typeOf(\$�args).' given');\n", + substr($type, 0, -3), + $i + 1, + $declaration->name() + ); + } else if (isset($parameters[$i])) { + if ($parameters[$i]->hasDefault()) { + $checks.= ' if ('.$parameters[$i]->defaultValue().' !== $'.$parameters[$i]->name().') '; + } else { + $checks.= ' '; + } + $checks.= sprintf( + "is('%s', $%s) || raise('lang.IllegalArgumentException', 'Argument #%d passed to %s must be of %1\$s, '.\xp::typeOf(\$%2\$s).' given');\n", + $type, + $parameters[$i]->name(), + $i + 1, + $declaration->name() + ); + } + } + + $tv= ''; + foreach ($placeholders as $key => $val) { + $tv.= ' $'.$key."= \lang\Type::forName('".$val."');\n"; + } + + $body= $declaration->body(); + return sprintf( + " %s function %s(%s)%s\n", + implode(' ', Modifiers::namesOf($declaration->modifiers())), + $declaration->name(), + substr($signature, 2), + $body ? " {\n".$checks.$tv.' '.$body."\n }" : ';' + ); + } + + /** + * Returns field declaration + */ + private function fieldDeclaration($declaration) { + $initial= $declaration->initial(); + return sprintf( + " %s $%s%s;\n", + implode(' ', Modifiers::namesOf($declaration->modifiers())), + $declaration->name(), + $initial ? '= '.$initial : '' + ); + } + + /** + * Returns constant declaration + */ + private function constantDeclaration($declaration) { + return sprintf( + " const %s = %s\n", + $declaration->name(), + $declaration->value() + ); + } } \ No newline at end of file diff --git a/src/main/php/lang/codedom/ConstantDeclaration.class.php b/src/main/php/lang/codedom/ConstantDeclaration.class.php index e23a8d54c2..6cd28c15a8 100755 --- a/src/main/php/lang/codedom/ConstantDeclaration.class.php +++ b/src/main/php/lang/codedom/ConstantDeclaration.class.php @@ -1,29 +1,32 @@ initial= $initial; + $this->value= $value; } /** @return bool */ public function isConstant() { return true; } + /** @return string */ + public function value() { return $this->value; } + /** * Creates a string representation * * @return string */ public function toString() { - return sprintf('%s@<%s = %s>', $this->getClassName(), $this->name, $this->initial); + return sprintf('%s@<%s = %s>', $this->getClassName(), $this->name, $this->value); } /** @@ -35,7 +38,7 @@ public function toString() { public function equals($cmp) { return $cmp instanceof self && ( $this->name === $cmp->name && - $this->initial === $cmp->initial + $this->value === $cmp->value ); } } \ No newline at end of file diff --git a/src/main/php/lang/codedom/Expr.class.php b/src/main/php/lang/codedom/Expr.class.php index a1c599cca7..612f06ff40 100755 --- a/src/main/php/lang/codedom/Expr.class.php +++ b/src/main/php/lang/codedom/Expr.class.php @@ -19,7 +19,7 @@ public function consume($rules, $stream, $values) { $braces--; $expr.= ']'; } else if (')' === $t) { - $array--; + if (--$array < 0) return new Values(trim($expr)); $expr.= ')'; } else if ((',' === $t || ';' === $t) && (0 === $braces && 0 === $array)) { return new Values(trim($expr)); diff --git a/src/main/php/lang/codedom/FieldDeclaration.class.php b/src/main/php/lang/codedom/FieldDeclaration.class.php index b5c58f001c..cefceea39b 100755 --- a/src/main/php/lang/codedom/FieldDeclaration.class.php +++ b/src/main/php/lang/codedom/FieldDeclaration.class.php @@ -13,6 +13,9 @@ public function __construct($modifiers, $annotations, $name, $initial) { /** @return bool */ public function isField() { return true; } + /** @return string */ + public function initial() { return $this->initial; } + /** * Creates a string representation * diff --git a/src/main/php/lang/codedom/MethodDeclaration.class.php b/src/main/php/lang/codedom/MethodDeclaration.class.php index 61eda527b7..1df896147a 100755 --- a/src/main/php/lang/codedom/MethodDeclaration.class.php +++ b/src/main/php/lang/codedom/MethodDeclaration.class.php @@ -12,7 +12,7 @@ class MethodDeclaration extends MemberDeclaration { * @param int $modifiers * @param string $annotations * @param string $name - * @param string[] $parameters Argument types + * @param string[] $parameters Parameter types * @param string $returns Return type * @param string[] $throws Exception types * @param string $body Code in body as string @@ -52,7 +52,7 @@ public function toString() { $this->annotations ? $this->annotations.' ' : '', implode(' ', Modifiers::namesOf($this->modifiers)), $this->name, - implode(', ', $this->parameters), + implode(', ', array_map(function($p) { return $p->toString(); }, $this->parameters)), $this->returns, $this->throws ? ' throws '.implode(' ', $this->throws) : '', $this->body ? ' { '.strlen($this->body).' bytes }' : '' diff --git a/src/main/php/lang/codedom/Parameter.class.php b/src/main/php/lang/codedom/Parameter.class.php new file mode 100755 index 0000000000..d371892f7b --- /dev/null +++ b/src/main/php/lang/codedom/Parameter.class.php @@ -0,0 +1,69 @@ +name= $name; + $this->restriction= $restriction; + $this->type= $type; + $this->default= $default; + } + + /** @return string */ + public function name() { return $this->name; } + + /** @return string */ + public function restriction() { return $this->restriction; } + + /** @return bool */ + public function hasDefault() { return null !== $this->default; } + + /** @return string */ + public function defaultValue() { return $this->default; } + + /** @return string */ + public function type() { return $this->type; } + + /** @param string $type */ + public function typed($type) { $this->type= $type; } + + /** @param string $default */ + public function orElse($default) { $this->default= $default; } + + /** + * Creates a string representation + * + * @return string + */ + public function toString() { + return sprintf( + '%s%s%s', + $this->restriction ? $this->restriction.' ' : '', + $this->name, + $this->default ? '= '.$this->default : '' + ); + } + + /** + * Returns whether a given value is equal to this code unit + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && ( + $this->name === $cmp->name && + $this->restriction === $cmp->restriction && + $this->default === $cmp->default + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/codedom/PhpSyntax.class.php b/src/main/php/lang/codedom/PhpSyntax.class.php index e785a68646..d9c490482c 100755 --- a/src/main/php/lang/codedom/PhpSyntax.class.php +++ b/src/main/php/lang/codedom/PhpSyntax.class.php @@ -96,12 +96,16 @@ function($values) { $modifiers, new Rule(':annotations'), // Old way of annotating fields, in combination with grouped syntax new AnyOf([ - T_FUNCTION => new Sequence([new Token(T_STRING), new Token('('), new SkipOver('(', ')'), new Rule(':method')], function($values, $stream) { + T_FUNCTION => new Sequence([new Token(T_STRING), new Token('('), new Optional(new ListOf(new Rule(':param'))), new Token(')'), new Rule(':method')], function($values, $stream) { $details= self::details($stream->comment()); + $params= (array)$values[3]; + foreach ($params as $i => $param) { + isset($details['params'][$i]) && $param->typed($details['params'][$i]); + } if ('__construct' === $values[1]) { - return new ConstructorDeclaration(0, null, $values[1], $details['args'], $details['throws'], $values[4]); + return new ConstructorDeclaration(0, null, $values[1], $params, $details['throws'], $values[5]); } else { - return new MethodDeclaration(0, null, $values[1], $details['args'], $details['returns'], $details['throws'], $values[4]); + return new MethodDeclaration(0, null, $values[1], $params, $details['returns'], $details['throws'], $values[5]); } }), T_VARIABLE => new Sequence([new Rule(':field')], function($values) { @@ -117,6 +121,18 @@ function($values) { } ), ]), + ':param' => new Sequence([ + new AnyOf([ + T_VARIABLE => new Sequence([], function($values) { return new Parameter(substr($values[0], 1), null); }), + T_ARRAY => new Sequence([new Token(T_VARIABLE)], function($values) { return new Parameter(substr($values[1], 1), 'array'); }), + T_CALLABLE => new Sequence([new Token(T_VARIABLE)], function($values) { return new Parameter(substr($values[1], 1), 'callable'); }), + ], [ + new Sequence([$type, new Token(T_VARIABLE)], function($values) { return new Parameter(substr($values[1], 1), implode('', $values[0])); }) + ]), + new Optional(new Sequence([new Token('='), new Expr()], function($values) { return $values[1]; })) + ], + function($values) { $values[0]->orElse($values[1]); return $values[0]; } + ), ':const' => new Sequence([new Token(T_STRING), new Token('='), new Expr()], function($values) { return new ConstantDeclaration($values[0], $values[2]); }), @@ -208,10 +224,10 @@ private static function details($comment) { preg_match_all('/@([a-z]+)\s*([^\r\n]+)?/', $comment, $matches, PREG_SET_ORDER); $arg= 0; - $details= ['args' => [], 'returns' => 'var', 'throws' => []]; + $details= ['params' => [], 'returns' => 'var', 'throws' => []]; foreach ($matches as $match) { if ('param' === $match[1]) { - $details['args'][$arg++]= self::typeIn($match[2]); + $details['params'][$arg++]= self::typeIn($match[2]); } else if ('return' === $match[1]) { $details['returns']= self::typeIn($match[2]); } else if ('throws' === $match[1]) { diff --git a/src/main/php/lang/codedom/Stream.class.php b/src/main/php/lang/codedom/Stream.class.php index 2844363b73..e1839a5c50 100755 --- a/src/main/php/lang/codedom/Stream.class.php +++ b/src/main/php/lang/codedom/Stream.class.php @@ -38,7 +38,7 @@ public function token($whitespace= false) { $continue= true; do { - $next= $this->stream[$this->offset]; + $next= @$this->stream[$this->offset]; if (T_DOC_COMMENT === $next[0]) { $this->comment= $next[1]; $this->line= $next[2]; diff --git a/src/main/php/lang/reflect/ClassParser.class.php b/src/main/php/lang/reflect/ClassParser.class.php index c1fa21fd91..dc6c087bcf 100755 --- a/src/main/php/lang/reflect/ClassParser.class.php +++ b/src/main/php/lang/reflect/ClassParser.class.php @@ -66,7 +66,7 @@ public function parseDetails($bytes, $context= '') { $details[0][$member->name()]= [DETAIL_ANNOTATIONS => $annotations[0]]; } else if ($member->isMethod()) { $details[1][$member->name()]= [ - DETAIL_ARGUMENTS => $member->parameters(), + DETAIL_ARGUMENTS => array_map(function($p) { return $p->type(); }, $member->parameters()), DETAIL_RETURNS => $member->returns(), DETAIL_THROWS => $member->throws(), DETAIL_ANNOTATIONS => $annotations[0], diff --git a/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php b/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php index 47bb02c161..69ab99a705 100755 --- a/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php +++ b/src/test/php/net/xp_framework/unittest/core/PhpSyntaxTest.class.php @@ -11,6 +11,7 @@ use lang\codedom\ConstantDeclaration; use lang\codedom\TraitUsage; use lang\codedom\TypeBody; +use lang\codedom\Parameter; /** * Integration test for lang.codedom package @@ -182,12 +183,17 @@ public function class_with_method_with_code() { } #[@test] - public function class_with_method_with_arguments() { + public function class_with_method_with_parameters() { + $parameters= [ + new Parameter('a', null, '1'), + new Parameter('b', 'Generic'), + new Parameter('c', 'array', 'array(1)') + ]; $this->assertEquals( new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ - new MethodDeclaration(0, null, 'test', [], 'var', [], 'return true;') + new MethodDeclaration(0, null, 'test', $parameters, 'var', [], 'return true;') ]))), - (new PhpSyntax())->parse('parse('assertEquals( new CodeUnit(null, [], new ClassDeclaration(0, null, 'Test', 'Object', [], new TypeBody([ - new MethodDeclaration(0, null, 'test', ['string'], 'bool', ['lang.Throwable'], 'return true;') + new MethodDeclaration(0, null, 'test', [new Parameter('name', null, null, 'string')], 'bool', ['lang.Throwable'], 'return true;') ]))), (new PhpSyntax())->parse('', $this->name From b48a6ffb1877883e91eb272d5e8411fa6dcbbcaf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 26 Feb 2015 20:57:28 +0100 Subject: [PATCH 22/23] Use pre-initialized __generic[] type arg lookup --- src/main/php/lang/GenericTypes.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index 5e52c22e51..206eefcce5 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -273,7 +273,7 @@ private function methodDeclaration($declaration, $generic, $placeholders) { $tv= ''; foreach ($placeholders as $key => $val) { - $tv.= ' $'.$key."= \lang\Type::forName('".$val."');\n"; + $tv.= ' $'.$key."= self::\$__generic['".$key."'];\n"; } $body= $declaration->body(); From 7db28592e17a2d5862aee203c494016150f714a1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 26 Feb 2015 22:01:24 +0100 Subject: [PATCH 23/23] QA: Apidocs --- src/main/php/lang/GenericTypes.class.php | 39 ++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index 206eefcce5..7c23198d4c 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -153,6 +153,13 @@ public function newType0($base, $arguments) { return $name; } + /** + * Resolve generic annotation + * + * @param string $annotations + * @param lang.XPClass $base + * @param [:string] $imports + */ private function genericAnnotation($annotations, $base, $imports) { if (null === $annotations) { return []; @@ -162,6 +169,13 @@ private function genericAnnotation($annotations, $base, $imports) { } } + /** + * Returns type arguments + * + * @param string $annotation + * @param [:string] $placeholders + * @return lang.Type[] + */ private function typeArgs($annotation, $placeholders) { $args= []; foreach (explode(',', $annotation) as $j => $placeholder) { @@ -172,6 +186,13 @@ private function typeArgs($annotation, $placeholders) { /** * Returns class declaration + * + * @param string $name + * @param lang.XPClass $base + * @param lang.codedom.ClassDeclaration $declaration + * @param [:string] $generic Generic class annotation + * @param [:string] $placeholders + * @return string */ private function classDeclaration($name, $base, $declaration, $generic, $placeholders) { if (isset($generic['parent'])) { @@ -207,6 +228,13 @@ private function classDeclaration($name, $base, $declaration, $generic, $placeho /** * Returns interface declaration + * + * @param string $name + * @param lang.XPClass $base + * @param lang.codedom.ClassDeclaration $declaration + * @param [:string] $generic Generic class annotation + * @param [:string] $placeholders + * @return string */ private function interfaceDeclaration($name, $base, $declaration, $generic, $placeholders) { $extends= []; @@ -229,6 +257,11 @@ private function interfaceDeclaration($name, $base, $declaration, $generic, $pla /** * Returns method declaration + * + * @param lang.codedom.MethodDeclaration $declaration + * @param string[] $generic Generic parameter types + * @param [:string] $placeholders + * @return string */ private function methodDeclaration($declaration, $generic, $placeholders) { $parameters= $declaration->parameters(); @@ -288,6 +321,9 @@ private function methodDeclaration($declaration, $generic, $placeholders) { /** * Returns field declaration + * + * @param lang.codedom.FieldDeclaration $declaration + * @return string */ private function fieldDeclaration($declaration) { $initial= $declaration->initial(); @@ -301,6 +337,9 @@ private function fieldDeclaration($declaration) { /** * Returns constant declaration + * + * @param lang.codedom.ConstantDeclaration $declaration + * @return string */ private function constantDeclaration($declaration) { return sprintf(