diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 698f01c83e..54ff04a792 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: Tests on: push: branches: - - master + - ten pull_request: jobs: diff --git a/ChangeLog.md b/ChangeLog.md index 24f6ef0436..49aa4bc449 100755 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,39 @@ XP Framework Core ChangeLog ## ?.?.? / ????-??-?? +## 10.16.0 / 2023-06-03 + +### Features + +* Backported #324 from XP 11: Support eval array notation inside attributes. + See https://github.com/xp-framework/compiler/pull/169#discussion_r1216367152 + (@thekid) + +### Bugfixes + +* Backported #321 from XP 11: ix util.TimeZone::hasDst(), which was broken in + multiple regards + (@thekid) +* Backported #320 from XP 11: Call to undefined method util.TimeZone::getName() + (@thekid) +* Backported #309 from XP 11: Use AES-128-CBC instead of DES (in util.Secret); + the latter is considered legacy + (@thekid) + +## 10.15.1 / 2021-12-24 + +### Bugfixes + +* Backported PR #306 from XP 11: Correctly support qualified annotation + names and trailing commas in grouped annotations + (@thekid) + +## 10.15.0 / 2021-10-21 + +### Features + +* Merged PR #301: Add new util.ClassPathPropertySource - @thekid + ## 10.14.0 / 2021-09-26 ### Features diff --git a/README.md b/README.md index 063d84dd03..738f4b264d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ the following one-liner: ```sh $ cd ~/bin -$ curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/i/installer/setup-8.5.3.sh | sh +$ curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/i/installer/setup-8.6.1.sh | sh ``` ### Using it @@ -36,7 +36,7 @@ Finally, start `xp -v` to see it working: ```sh $ xp -v -XP 10.14.1-dev { PHP/8.0.11 & Zend/4.0.11 } @ Windows NT SURFACE 10.0 build 22000 (Windows 10) AMD64 +XP 10.15.2-dev { PHP/8.0.14 & Zend/4.0.14 } @ Windows NT SURFACE 10.0 build 22000 (Windows 10) AMD64 Copyright (c) 2001-2021 the XP group FileSystemCL<./src/main/php> FileSystemCL<./src/test/php> diff --git a/src/main/php/lang/XPClass.class.php b/src/main/php/lang/XPClass.class.php index ff05603840..751ba7b38e 100755 --- a/src/main/php/lang/XPClass.class.php +++ b/src/main/php/lang/XPClass.class.php @@ -1,7 +1,7 @@ loadClassBytes($class))) return null; - $parser ?? $parser= new \lang\reflect\ClassParser(); - return \xp::$meta[$class]= $parser->parseDetails($bytes, $class); + $parser ?? $parser= new ClassParser(); + return \xp::$meta[$class]= $parser->parseDetails($bytes); } /** diff --git a/src/main/php/lang/reflect/ClassParser.class.php b/src/main/php/lang/reflect/ClassParser.class.php index 8d351b2e30..88af6be6b3 100755 --- a/src/main/php/lang/reflect/ClassParser.class.php +++ b/src/main/php/lang/reflect/ClassParser.class.php @@ -15,67 +15,50 @@ class ClassParser { /** - * Resolves a type in a given context. Recognizes classes imported via - * the `use` statement. + * Parses and resolves a type * - * @param string $type - * @param string $context + * @param var[] $tokens + * @param int $i + * @param [:string] $context * @param [:string] $imports - * @return lang.XPClass + * @return string * @throws lang.IllegalStateException */ - protected function resolve($type, $context, $imports) { - if ('self' === $type) { - return XPClass::forName($context); - } else if ('parent' === $type) { - if ($parent= XPClass::forName($context)->getParentclass()) return $parent; - throw new IllegalStateException('Class does not have a parent'); - } else if ('\\' === $type[0]) { - return new XPClass($type); - } 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) || trait_exists($type, false) || enum_exists($type, false)) { - return new XPClass($type); - } else if (false !== ($p= strrpos($context, '.'))) { - return XPClass::forName(substr($context, 0, $p + 1).$type); - } else { - throw new IllegalStateException('Cannot resolve '.$type); - } - } + protected function type($tokens, &$i, $context, $imports) { + if (T_NAME_FULLY_QUALIFIED === $tokens[$i][0]) { + return strtr(substr($tokens[$i][1], 1), '\\', '.'); + } else if (T_NS_SEPARATOR === $tokens[$i][0]) { + $type= ''; + do { + $type.= '.'.$tokens[$i + 1][1]; + $i+= 2; + } while (T_NS_SEPARATOR === $tokens[$i][0]); + + $i--; // Position at the last T_STRING token + return substr($type, 1); + } else if (T_NAME_QUALIFIED === $tokens[$i][0]) { + return $context['namespace'].strtr($tokens[$i][1], '\\', '.'); + } else if (T_STRING === $tokens[$i][0]) { + $type= $tokens[$i][1]; + if ('self' === $type) { + return $context['self']; + } else if ('parent' === $type) { + if (isset($context['parent'])) return $context['parent']; + throw new IllegalStateException('Class does not have a parent'); + } - /** - * 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() - )); + while (T_NS_SEPARATOR === $tokens[$i + 1][0]) { + $type.= '.'.$tokens[$i + 2][1]; + $i+= 2; } - } else if (T_CLASS === $token[0]) { - return $class->literal(); + return $imports[$type] ?? $context['namespace'].$type; + } else if (T_STATIC === $tokens[$i][0]) { + return $context['self']; } else { - return $class->getConstant($token[1]); + throw new IllegalStateException(sprintf( + 'Parse error: Unexpected %s', + is_array($tokens[$i]) ? token_name($tokens[$i][0]) : '"'.$tokens[$i].'"' + )); } } @@ -84,18 +67,18 @@ protected function memberOf($class, $token, $context) { * * @param var[] $tokens * @param int $i - * @param string $context + * @param [:string] $context * @param [:string] $imports * @return var */ - protected function valueOf($tokens, &$i, $context, $imports) { + protected function value($tokens, &$i, $context, $imports) { $token= $tokens[$i][0]; if ('-' === $token) { $i++; - return -1 * $this->valueOf($tokens, $i, $context, $imports); + return -1 * $this->value($tokens, $i, $context, $imports); } else if ('+' === $token) { $i++; - return +1 * $this->valueOf($tokens, $i, $context, $imports); + return +1 * $this->value($tokens, $i, $context, $imports); } else if (T_CONSTANT_ENCAPSED_STRING === $token) { return eval('return '.$tokens[$i][1].';'); } else if (T_LNUMBER === $tokens[$i][0]) { @@ -134,21 +117,12 @@ protected function valueOf($tokens, &$i, $context, $imports) { continue; } else { if ($element) throw new IllegalStateException('Parse error: Malformed array - missing comma'); - $element= [$this->valueOf($tokens, $i, $context, $imports)]; + $element= [$this->value($tokens, $i, $context, $imports)]; } } return $value; } else if ('"' === $token || T_ENCAPSED_AND_WHITESPACE === $token) { throw new IllegalStateException('Parse error: Unterminated string'); - } else if (T_NS_SEPARATOR === $token) { - $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_NAME_FULLY_QUALIFIED === $token) { - $type= $tokens[$i++][1]; - return $this->memberOf(XPClass::forName($type), $tokens[++$i], $context); } else if (T_FN === $token || T_STRING === $token && 'fn' === $tokens[$i][1]) { $s= sizeof($tokens); $b= 0; @@ -214,23 +188,11 @@ protected function valueOf($tokens, &$i, $context, $imports) { throw new IllegalStateException('In `'.$code.'`: '.ucfirst($error['message'])); } return $func; - } else if (T_STRING === $token) { // 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 { - throw new ElementNotFoundException('Undefined constant "'.$tokens[$i][1].'"'); - } } else if (T_NEW === $token) { - $type= ''; - $i++; - while ('(' !== $tokens[++$i]) { - $type.= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; - } - $i++; - $class= $this->resolve($type, $context, $imports); + $i+= 2; + $class= XPClass::forName($this->type($tokens, $i, $context, $imports)); + + $i+= 2; for ($args= [], $arg= null, $s= sizeof($tokens); ; $i++) { if (')' === $tokens[$i]) { $arg && $args[]= $arg[0]; @@ -239,7 +201,7 @@ protected function valueOf($tokens, &$i, $context, $imports) { $args[]= $arg[0]; $arg= null; } else if (T_WHITESPACE !== $tokens[$i][0]) { - $arg= [$this->valueOf($tokens, $i, $context, $imports)]; + $arg= [$this->value($tokens, $i, $context, $imports)]; } } return $class->newInstance(...$args); @@ -278,11 +240,34 @@ protected function valueOf($tokens, &$i, $context, $imports) { throw new IllegalStateException('In `'.$code.'`: '.ucfirst($error['message'])); } return $func; + } else if (T_STRING === $token && T_NS_SEPARATOR !== $tokens[$i + 1][0] && T_DOUBLE_COLON !== $tokens[$i + 1][0]) { + if (defined($tokens[$i][1])) return constant($tokens[$i][1]); + throw new ElementNotFoundException('Undefined constant "'.$tokens[$i][1].'"'); } else { - throw new IllegalStateException(sprintf( - 'Parse error: Unexpected %s', - is_array($tokens[$i]) ? token_name($token) : '"'.$tokens[$i].'"' - )); + $class= XPClass::forName($this->type($tokens, $i, $context, $imports)); + $i+= 2; + if (T_VARIABLE === $tokens[$i][0]) { + $field= $class->getField(substr($tokens[$i][1], 1)); + $m= $field->getModifiers(); + if ($m & MODIFIER_PUBLIC) { + return $field->get(null); + } else if (($m & MODIFIER_PROTECTED) && $class->isAssignableFrom($context['self'])) { + return $field->setAccessible(true)->get(null); + } else if (($m & MODIFIER_PRIVATE) && $class->getName() === $context['self']) { + 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 === $tokens[$i][0]) { + return $class->literal(); + } else { + return $class->getConstant($tokens[$i][1]); + } } } @@ -290,7 +275,7 @@ protected function valueOf($tokens, &$i, $context, $imports) { * Parses annotation string * * @param string bytes - * @param string context the class name + * @param [:string] context * @return [:string] imports * @param int line * @return [:var] @@ -303,9 +288,16 @@ public function parseAnnotations($bytes, $context, $imports= [], $line= -1) { 'multi-value' ]; + // BC when class name is passed for context + if (is_string($context)) { + $parent= get_parent_class(strtr($context, '.', '\\')); + $namespace= false === ($p= strrpos($context, '.')) ? '' : substr($context, 0, $p + 1); + $context= ['self' => $context, 'parent' => $parent ? strtr($parent, '\\', '.') : null, 'namespace' => $namespace]; + } + $tokens= token_get_all(' [], 1 => []]; - $place= $context.(-1 === $line ? '' : ', line '.$line); + $place= $context['self'].(-1 === $line ? '' : ', line '.$line); // Parse tokens try { @@ -319,13 +311,15 @@ public function parseAnnotations($bytes, $context, $imports= [], $line= -1) { $value= null; $i++; $state= 1; - } else if (T_STRING === $tokens[$i][0]) { - $annotation= lcfirst($tokens[$i][1]); + } else if (']' === $tokens[$i]) { // Handle situations with trailing comma + $annotations[0][$annotation]= $value; + return $annotations; + } else { + $type= $this->type($tokens, $i, $context, $imports); + $annotation= lcfirst(false === ($p= strrpos($type, '.')) ? $type : substr($type, $p + 1)); $param= null; $value= null; $state= 1; - } else { - throw new IllegalStateException('Parse error: Expecting "@"'); } } else if (1 === $state) { // Inside attribute, check for values if ('(' === $tokens[$i]) { @@ -350,7 +344,7 @@ public function parseAnnotations($bytes, $context, $imports= [], $line= -1) { } else if (T_STRING === $tokens[$i][0]) { $annotation= $tokens[$i][1]; } else { - throw new IllegalStateException('Parse error: Expecting either "(", "," or "]"'); + throw new IllegalStateException('Parse error: Expecting either "(", "," or "]", have '.(is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i])); } } else if (2 === $state) { // Inside braces of @attr(...) if (')' === $tokens[$i]) { @@ -360,10 +354,24 @@ public function parseAnnotations($bytes, $context, $imports= [], $line= -1) { if ('eval' === $key) { // Attribute(eval: '...') vs. Attribute(name: ...) while ($i++ < $s && ':' === $tokens[$i] || T_WHITESPACE === $tokens[$i][0]) { } - $code= $this->valueOf($tokens, $i, $context, $imports); - $eval= token_get_all('value($tokens, $i, $context, $imports); + if (is_string($code)) { + $eval= token_get_all(' $expr) { + $pairs.= "'".strtr($named, ["'" => "\\'"])."'=>{$expr},"; + } + $eval= token_get_all('valueOf($eval, $j, $context, $imports); + $value= $this->value($eval, $j, $context, $imports); } else { $value= []; $state= 3; @@ -374,7 +382,7 @@ public function parseAnnotations($bytes, $context, $imports= [], $line= -1) { $state= 3; trigger_error('Use of deprecated annotation key/value pair "'.$key.'" in '.$place, E_USER_DEPRECATED); } else { - $value= $this->valueOf($tokens, $i, $context, $imports); + $value= $this->value($tokens, $i, $context, $imports); } } else if (3 === $state) { // Parsing key inside named arguments if (')' === $tokens[$i]) { @@ -387,7 +395,7 @@ public function parseAnnotations($bytes, $context, $imports= [], $line= -1) { $key= $tokens[$i][1]; } } else if (4 === $state) { // Parsing value inside named arguments - $value[$key]= $this->valueOf($tokens, $i, $context, $imports); + $value[$key]= $this->value($tokens, $i, $context, $imports); $state= 3; } } @@ -456,16 +464,15 @@ public static function typeIn($text, $imports) { * Parse details from a given input string * * @param string bytes - * @param string context default '' * @return [:var] details */ - public function parseDetails($bytes, $context= '') { + public function parseDetails($bytes) { $details= [[], []]; $annotations= [0 => [], 1 => []]; $imports= []; $comment= ''; - $namespace= ''; $parsed= ''; + $context= ['namespace' => '', 'self' => null, 'parent' => null]; $tokens= token_get_all($bytes); for ($i= 0, $s= sizeof($tokens); $i < $s; $i++) { switch ($tokens[$i][0]) { @@ -474,7 +481,7 @@ public function parseDetails($bytes, $context= '') { for ($i+= 2; $i < $s, !(';' === $tokens[$i] || T_WHITESPACE === $tokens[$i][0]); $i++) { $namespace.= $tokens[$i][1]; } - $namespace.= '\\'; + $context['namespace']= strtr($namespace, '\\', '.').'.'; break; case T_USE: @@ -568,6 +575,7 @@ public function parseDetails($bytes, $context= '') { if (isset($details['class'])) break; // Inside class, e.g. $lookup= ['self' => self::class] case T_INTERFACE: case T_TRAIT: case T_ENUM: + $context['self']= $context['namespace'].$tokens[$i + 2][1]; if ($parsed) { $annotations= $this->parseAnnotations($parsed, $context, $imports, $tokens[$i][2] ?? -1); $parsed= ''; @@ -578,13 +586,17 @@ public function parseDetails($bytes, $context= '') { 4, // "/**\n" strpos($comment, '* @')- 2 // position of first details token ))), - DETAIL_ANNOTATIONS => $annotations[0], - DETAIL_ARGUMENTS => $namespace.$tokens[$i + 2][1] + DETAIL_ANNOTATIONS => $annotations[0] ]; $annotations= [0 => [], 1 => []]; $comment= ''; break; + case T_EXTENDS: + $i+= 2; + $context['parent']= $this->type($tokens, $i, $context, $imports); + break; + case T_VARIABLE: // Have a member variable if ($parsed) { $annotations= $this->parseAnnotations($parsed, $context, $imports, $tokens[$i][2] ?? -1); diff --git a/src/main/php/util/ClassPathPropertySource.class.php b/src/main/php/util/ClassPathPropertySource.class.php new file mode 100755 index 0000000000..4dc157314a --- /dev/null +++ b/src/main/php/util/ClassPathPropertySource.class.php @@ -0,0 +1,84 @@ +root= $path; + $this->loader= $loader ?? ClassLoader::getDefault(); + } + + /** + * Check whether source provides given properies + * + * @param string name + * @return bool + */ + public function provides($name) { + return $this->loader->providesResource((null === $this->root ? '' : $this->root.'/').$name.'.ini'); + } + + /** + * Load properties by given name + * + * @param string name + * @return util.Properties + * @throws lang.IllegalArgumentException if property requested is not available + */ + public function fetch($name) { + $resource= (null === $this->root ? '' : $this->root.'/').$name.'.ini'; + if (!$this->loader->providesResource($resource)) { + throw new IllegalArgumentException(sprintf( + 'No properties %s found at %s%s', + $name, + null === $this->root ? '' : $this->root.' @ ', + $this->loader->toString() + )); + } + + $p= new Properties(); + $p->load($this->loader->getResourceAsStream($resource)); + return $p; + } + + /** + * Returns hashcode for this source + * + * @return string + */ + public function hashCode() { + return 'CL'.md5($this->root); + } + + /** + * Check if this instance equals another + * + * @param var $cmp + * @return bool + */ + public function equals($cmp) { + return $cmp instanceof self && $cmp->root === $this->root && 0 === $this->loader->compareTo($cmp->loader); + } + + /** + * Creates a string representation of this object + * + * @return string + */ + public function toString() { + return nameof($this).'<'.$this->root.'>'; + } +} diff --git a/src/main/php/util/FilesystemPropertySource.class.php b/src/main/php/util/FilesystemPropertySource.class.php index 39a43fe99d..95040c14ca 100755 --- a/src/main/php/util/FilesystemPropertySource.class.php +++ b/src/main/php/util/FilesystemPropertySource.class.php @@ -5,12 +5,10 @@ /** * Filesystem-based property source * - * @deprecated - * @test xp://net.xp_framework.unittest.util.FilesystemPropertySourceTest + * @test net.xp_framework.unittest.util.FilesystemPropertySourceTest */ class FilesystemPropertySource implements PropertySource { protected $root; - protected $cache= []; /** * Constructor @@ -28,7 +26,6 @@ public function __construct($path) { * @return bool */ public function provides($name) { - if (isset($this->cache[$name])) return true; return file_exists($this->root.DIRECTORY_SEPARATOR.$name.'.ini'); } @@ -44,11 +41,7 @@ public function fetch($name) { throw new IllegalArgumentException('No properties '.$name.' found at '.$this->root); } - if (!isset($this->cache[$name])) { - $this->cache[$name]= new Properties($this->root.DIRECTORY_SEPARATOR.$name.'.ini'); - } - - return $this->cache[$name]; + return new Properties($this->root.DIRECTORY_SEPARATOR.$name.'.ini'); } /** @@ -57,7 +50,7 @@ public function fetch($name) { * @return string */ public function hashCode() { - return md5($this->root); + return 'FS'.md5($this->root); } /** diff --git a/src/main/php/util/PropertySource.class.php b/src/main/php/util/PropertySource.class.php index bfe6287dd5..f05fbe4696 100755 --- a/src/main/php/util/PropertySource.class.php +++ b/src/main/php/util/PropertySource.class.php @@ -1,26 +1,21 @@ name) + if ($name !== $this->name) { throw new IllegalArgumentException('Access to property source under wrong name "'.$name.'"'); + } return $this->prop; } @@ -51,7 +51,7 @@ public function fetch($name) { * @return string */ public function hashCode() { - return md5($this->name.serialize($this->prop)); + return 'RP'.md5($this->name.serialize($this->prop)); } /** @@ -61,9 +61,6 @@ public function hashCode() { * @return bool */ public function equals($cmp) { - return $cmp instanceof self && - $cmp->name === $this->name && - $this->prop->equals($cmp->prop) - ; + return $cmp instanceof self && $cmp->name === $this->name && $this->prop->equals($cmp->prop); } } diff --git a/src/main/php/util/Secret.class.php b/src/main/php/util/Secret.class.php index 965b65df1f..50b28337dd 100755 --- a/src/main/php/util/Secret.class.php +++ b/src/main/php/util/Secret.class.php @@ -1,6 +1,7 @@ id= uniqid(microtime(true)); - $this->update($characters); - } - - /** - * Update with given characters - * - * @param string $characters - * @return void - */ - protected function update(&$characters) { try { self::$store[$this->id]= self::$encrypt->__invoke((string)$characters); } catch (\Throwable $e) { diff --git a/src/main/php/util/TimeZone.class.php b/src/main/php/util/TimeZone.class.php index 6105aea767..1821b24065 100755 --- a/src/main/php/util/TimeZone.class.php +++ b/src/main/php/util/TimeZone.class.php @@ -113,10 +113,17 @@ public function getOffset($date= null) { /** * Retrieve whether the timezone does have DST/non-DST mode * - * @return bool + * @param ?int $year + * @return bool */ - public function hasDst() { - return (bool)sizeof(timezone_transitions_get($this->tz)); + public function hasDst($year= null) { + $year ?? $year= idate('Y'); + $t= timezone_transitions_get($this->tz, gmmktime(0, 0, 0, 1, 1, $year), gmmktime(0, 0, 0, 1, 1, $year + 1)); + + // Without DST: [2022-01-01 => UTC] + // With DST : [2022-01-01 => CET, 2022-03-27 => CEST, 2022-10-30 => CET] + // With 2*DST : [1945-01-01 => CET, 1945-04-02 => CEST, 1945-05-24 => CEMT, 1945-09-24 => CEST, 1945-11-18 => CET] + return sizeof($t) > 1; } /** @@ -185,7 +192,7 @@ public function nextTransition(Date $date) { * @return int */ public function compareTo($value) { - return $value instanceof self ? $this->getName() <=> $value->getName() : 1; + return $value instanceof self ? $this->name() <=> $value->name() : 1; } /** @@ -194,7 +201,7 @@ public function compareTo($value) { * @return string */ public function hashCode() { - return $this->getName(); + return $this->name(); } /** @@ -203,6 +210,6 @@ public function hashCode() { * @return string */ public function toString() { - return nameof($this).' ("'.$this->getName().'" / '.$this->getOffset().')'; + return nameof($this).'("'.$this->name().'")'; } } \ No newline at end of file diff --git a/src/main/resources/VERSION b/src/main/resources/VERSION index 546656c19c..89da89da65 100644 --- a/src/main/resources/VERSION +++ b/src/main/resources/VERSION @@ -1 +1 @@ -10.14.1-dev \ No newline at end of file +10.16.0 \ No newline at end of file diff --git a/src/test/config/unittest/util.ini b/src/test/config/unittest/util.ini old mode 100644 new mode 100755 index 79df95282b..1c45b6dd0d --- a/src/test/config/unittest/util.ini +++ b/src/test/config/unittest/util.ini @@ -51,9 +51,15 @@ class="net.xp_framework.unittest.util.MimeTypeTest" [observable] class="net.xp_framework.unittest.util.ObservableTest" -[fs-propertysource] +[filesystem-propertysource] class="net.xp_framework.unittest.util.FilesystemPropertySourceTest" +[classpath-propertysource] +class="net.xp_framework.unittest.util.ClassPathPropertySourceTest" + +[registered-propertysource] +class="net.xp_framework.unittest.util.RegisteredPropertySourceTest" + [more-power] class="net.xp_framework.unittest.util.BinfordTest" 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 a77c05129a..410c2df91a 100755 --- a/src/test/php/net/xp_framework/unittest/annotations/BrokenAnnotationTest.class.php +++ b/src/test/php/net/xp_framework/unittest/annotations/BrokenAnnotationTest.class.php @@ -39,21 +39,6 @@ public function unterminated_double_quoted_string_literal() { $this->parse('#[@attribute("value)]'); } - #[Test, Expect(['class' => ClassFormatException::class, 'withMessage' => '/Expecting "@"/'])] - public function missing_annotation_after_comma_and_value() { - $this->parse('#[@ignore("Test"), ]'); - } - - #[Test, Expect(['class' => ClassFormatException::class, 'withMessage' => '/Expecting "@"/'])] - public function missing_annotation_after_comma() { - $this->parse('#[@ignore, ]'); - } - - #[Test, Expect(['class' => ClassFormatException::class, 'withMessage' => '/Expecting "@"/'])] - public function missing_annotation_after_second_comma() { - $this->parse('#[@ignore, @test, ]'); - } - #[Test, Expect(['class' => ClassFormatException::class, 'withMessage' => '/Parse error: Unterminated string/'])] public function unterminated_dq_string() { $this->parse('#[@ignore("Test)]'); 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 c9bd0252d1..56ec6dadaa 100755 --- a/src/test/php/net/xp_framework/unittest/reflection/ClassDetailsTest.class.php +++ b/src/test/php/net/xp_framework/unittest/reflection/ClassDetailsTest.class.php @@ -38,7 +38,7 @@ public function test() { } public function parses($kind) { $details= (new ClassParser())->parseDetails('assertEquals( - [DETAIL_COMMENT => '', DETAIL_ANNOTATIONS => [], DETAIL_ARGUMENTS => 'Test'], + [DETAIL_COMMENT => '', DETAIL_ANNOTATIONS => []], $details['class'] ); } @@ -252,9 +252,9 @@ class Test { #[Test] public function use_statements_with_alias_evaluated() { $actual= (new ClassParser())->parseDetails('parseDetails('assertEquals(['test' => null, 'value' => 'test'], $actual['class'][DETAIL_ANNOTATIONS]); + } + + #[Test, Values(['\net\xp_framework\unittest\Name', 'unittest\Name', 'Name'])] + public function php8_attributes_with_named_arguments($name) { + $actual= (new ClassParser())->parseDetails('assertEquals('test', $actual['class'][DETAIL_ANNOTATIONS]['value']()); } + #[Test] + public function php8_attributes_with_array_eval_argument() { + $actual= (new ClassParser())->parseDetails('assertEquals('test', $actual['class'][DETAIL_ANNOTATIONS]['value']()); + } + + #[Test] + public function php8_attributes_with_named_array_eval_argument() { + $actual= (new ClassParser())->parseDetails(' "function() { return \"test\"; }"])] + class Test { + } + '); + $this->assertEquals('test', $actual['class'][DETAIL_ANNOTATIONS]['value']['func']()); + } + + #[Test] + public function php8_attributes_with_multiple_named_array_eval_arguments() { + $actual= (new ClassParser())->parseDetails(' "1", "two" => "2"])] + class Test { + } + '); + $this->assertEquals(['one' => 1, 'two' => 2], $actual['class'][DETAIL_ANNOTATIONS]['value']); + } + + #[Test] + public function absolute_compound_php8_attributes() { + $actual= (new ClassParser())->parseDetails('assertEquals(['test' => null], $actual['class'][DETAIL_ANNOTATIONS]); + } + + #[Test] + public function relative_compound_php8_attributes() { + $actual= (new ClassParser())->parseDetails('assertEquals(['test' => null], $actual['class'][DETAIL_ANNOTATIONS]); + } + + #[Test, Expect(class: ClassFormatException::class, withMessage: 'Unexpected ","')] + public function php8_attributes_cannot_have_multiple_arguments() { + (new ClassParser())->parseDetails('parseDetails('parseDetails('assertEquals(['test' => null], $details[0]['fixture'][DETAIL_ANNOTATIONS]); } + #[Test, Values(['\net\xp_framework\unittest\Name', 'unittest\Name', 'Name'])] + public function annotation_with_reference_to($parent) { + $details= (new ClassParser())->parseDetails('assertEquals(['fixture' => new Name('Test')], $details[1]['fixture'][DETAIL_ANNOTATIONS]); + } + #[Test, Expect(['class' => ClassFormatException::class, 'withMessage' => '/Class does not have a parent/'])] public function annotation_with_parent_reference_in_parentless_class() { (new ClassParser())->parseDetails('parseDetails('assertEquals( - [DETAIL_COMMENT => '', DETAIL_ANNOTATIONS => [], DETAIL_ARGUMENTS => 'Test'], + [DETAIL_COMMENT => '', DETAIL_ANNOTATIONS => []], $details['class'] ); } @@ -557,7 +651,7 @@ public function run() { } '); $this->assertEquals( - [DETAIL_COMMENT => 'Comment', DETAIL_ANNOTATIONS => [], DETAIL_ARGUMENTS => 'Test'], + [DETAIL_COMMENT => 'Comment', DETAIL_ANNOTATIONS => []], $details['class'] ); } diff --git a/src/test/php/net/xp_framework/unittest/util/ClassPathPropertySourceTest.class.php b/src/test/php/net/xp_framework/unittest/util/ClassPathPropertySourceTest.class.php new file mode 100755 index 0000000000..2696b1d8b5 --- /dev/null +++ b/src/test/php/net/xp_framework/unittest/util/ClassPathPropertySourceTest.class.php @@ -0,0 +1,48 @@ +fixture= new ClassPathPropertySource(null, new FileSystemClassLoader($tempDir)); + + // Create a temporary ini file + $this->tempFile= new File($tempDir, 'temp.ini'); + Files::write($this->tempFile, "[section]\nkey=value\n"); + } + + /** @return void */ + public function tearDown() { + $this->tempFile->unlink(); + } + + #[Test] + public function provides_existing_ini_file() { + $this->assertTrue($this->fixture->provides('temp')); + } + + #[Test] + public function does_not_provide_non_existant_ini_file() { + $this->assertFalse($this->fixture->provides('@@non-existant@@')); + } + + #[Test] + public function fetch_existing_ini_file() { + $this->assertEquals( + ['key' => 'value'], + $this->fixture->fetch('temp')->readSection('section') + ); + } + + #[Test, Expect(class: IllegalArgumentException::class, withMessage: '/No properties @@non-existant@@ found at .+/')] + public function fetch_non_existant_ini_file() { + $this->fixture->fetch('@@non-existant@@'); + } +} \ No newline at end of file diff --git a/src/test/php/net/xp_framework/unittest/util/FilesystemPropertySourceTest.class.php b/src/test/php/net/xp_framework/unittest/util/FilesystemPropertySourceTest.class.php index 36748bd506..fe7d61540c 100755 --- a/src/test/php/net/xp_framework/unittest/util/FilesystemPropertySourceTest.class.php +++ b/src/test/php/net/xp_framework/unittest/util/FilesystemPropertySourceTest.class.php @@ -5,12 +5,6 @@ use unittest\{Expect, Test, TestCase}; use util\{FilesystemPropertySource, Properties}; -/** - * Testcase for util.Properties class. - * - * @deprecated - * @see xp://util.FilesystemPropertySource - */ class FilesystemPropertySourceTest extends TestCase { protected $tempFile, $fixture; diff --git a/src/test/php/net/xp_framework/unittest/util/RegisteredPropertySourceTest.class.php b/src/test/php/net/xp_framework/unittest/util/RegisteredPropertySourceTest.class.php index a7418c8c68..ba27530256 100755 --- a/src/test/php/net/xp_framework/unittest/util/RegisteredPropertySourceTest.class.php +++ b/src/test/php/net/xp_framework/unittest/util/RegisteredPropertySourceTest.class.php @@ -4,41 +4,24 @@ use unittest\{Test, TestCase}; use util\{RegisteredPropertySource, Properties}; -/** - * Test for RegisteredPropertySource - * - * @deprecated - * @see xp://util.RegisteredPropertySource - */ class RegisteredPropertySourceTest extends TestCase { - protected $fixture= null; + protected $fixture; + /** @return void */ public function setUp() { $this->fixture= new RegisteredPropertySource('props', new Properties(null)); } - /** - * Test - * - */ #[Test] public function doesNotHaveAnyProperties() { $this->assertFalse($this->fixture->provides('properties')); } - /** - * Test - * - */ #[Test] public function hasRegisteredProperty() { $this->assertTrue($this->fixture->provides('props')); } - /** - * Test - * - */ #[Test] public function returnsRegisteredProperties() { $p= new Properties(null); @@ -47,10 +30,6 @@ public function returnsRegisteredProperties() { $this->assertTrue($p === $m->fetch('name')); } - /** - * Test - * - */ #[Test] public function equalsReturnsFalseForDifferingName() { $p1= new RegisteredPropertySource('name1', new Properties(null)); @@ -59,10 +38,6 @@ public function equalsReturnsFalseForDifferingName() { $this->assertNotEquals($p1, $p2); } - /** - * Test - * - */ #[Test] public function equalsReturnsFalseForDifferingProperties() { $p1= new RegisteredPropertySource('name1', new Properties(null)); @@ -71,10 +46,6 @@ public function equalsReturnsFalseForDifferingProperties() { $this->assertNotEquals($p1, $p2); } - /** - * Test - * - */ #[Test] public function equalsReturnsTrueForSameInnerPropertiesAndName() { $p1= new RegisteredPropertySource('name1', (new Properties(null))->load(new MemoryInputStream('[section]'))); diff --git a/src/test/php/net/xp_framework/unittest/util/TimeZoneTest.class.php b/src/test/php/net/xp_framework/unittest/util/TimeZoneTest.class.php index b56201b7e8..c0708e8d61 100755 --- a/src/test/php/net/xp_framework/unittest/util/TimeZoneTest.class.php +++ b/src/test/php/net/xp_framework/unittest/util/TimeZoneTest.class.php @@ -40,7 +40,7 @@ public function translate() { } #[Test] - public function previousTransition() { + public function previous_transition() { $transition= (new TimeZone('Europe/Berlin'))->previousTransition(new Date('2007-08-23')); $this->assertEquals(true, $transition->isDst()); $this->assertEquals('CEST', $transition->abbr()); @@ -49,32 +49,35 @@ public function previousTransition() { } #[Test] - public function previousPreviousTransition() { - $transition= (new TimeZone('Europe/Berlin'))->previousTransition(new Date('2007-08-23')); + public function previous_previous_transition() { + $tz= new TimeZone('Europe/Berlin'); + $transition= $tz->previousTransition(new Date('2007-08-23')); $previous= $transition->previous(); $this->assertFalse($previous->isDst()); $this->assertEquals('CET', $previous->abbr()); $this->assertEquals('+0100', $previous->difference()); - $this->assertEquals(new Date('2006-10-29 02:00:00 Europe/Berlin'), $previous->date()); + $this->assertEquals(new Date('2006-10-29 02:00:00', $tz), $previous->date()); } #[Test] - public function previousNextTransition() { - $transition= (new TimeZone('Europe/Berlin'))->previousTransition(new Date('2007-08-23')); + public function previous_next_transition() { + $tz= new TimeZone('Europe/Berlin'); + $transition= $tz->previousTransition(new Date('2007-08-23')); $next= $transition->next(); $this->assertFalse($next->isDst()); $this->assertEquals('CET', $next->abbr()); $this->assertEquals('+0100', $next->difference()); - $this->assertEquals(new Date('2007-10-28 02:00:00 Europe/Berlin'), $next->date()); + $this->assertEquals(new Date('2007-10-28 02:00:00', $tz), $next->date()); } #[Test] - public function nextTransition() { - $transition= (new TimeZone('Europe/Berlin'))->nextTransition(new Date('2007-08-23')); + public function next_transition() { + $tz= new TimeZone('Europe/Berlin'); + $transition= $tz->nextTransition(new Date('2007-08-23')); $this->assertEquals(false, $transition->isDst()); $this->assertEquals('CET', $transition->abbr()); $this->assertEquals('+0100', $transition->difference()); - $this->assertEquals(new Date('2007-10-28 02:00:00 Europe/Berlin'), $transition->date()); + $this->assertEquals(new Date('2007-10-28 02:00:00', $tz), $transition->date()); } #[Test, Expect(IllegalArgumentException::class)] @@ -125,4 +128,33 @@ public function offsetInSecondsDST() { public function offsetInSecondsNoDST() { $this->assertEquals(3600, (new TimeZone('Europe/Berlin'))->getOffsetInSeconds(new Date('2007-01-21'))); } + + #[Test, Values([['Europe/Berlin', 2022, true], ['Europe/Berlin', 1977, false], ['Europe/Berlin', 1945, true], ['Atlantic/Reykjavik', 2022, false], ['Atlantic/Reykjavik', 1966, true], ['UTC', 2022, false]])] + public function has_dst($name, $year, $expected) { + $this->assertEquals($expected, TimeZone::getByName($name)->hasDst($year)); + } + + #[Test] + public function name_used_as_hashcode() { + $this->assertEquals( + 'Europe/Berlin', + TimeZone::getByName('Europe/Berlin')->hashCode() + ); + } + + #[Test] + public function string_representation() { + $this->assertEquals( + 'util.TimeZone("Europe/Berlin")', + TimeZone::getByName('Europe/Berlin')->toString() + ); + } + + #[Test, Values([['Europe/Berlin', 0], ['Europe/Paris', -1], ['America/New_York', 1]])] + public function compare_to($name, $expected) { + $this->assertEquals( + $expected, + TimeZone::getByName('Europe/Berlin')->compareTo(TimeZone::getByName($name)) + ); + } } \ No newline at end of file