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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/BuiltinInternalArgInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,8 @@ public static function stubReturnTypeLabelForFunction(string $callableLc): ?stri
'grapheme_str_split' => 'array|false',
// ext/intl/php_intl.stub.php — PHP 8.5+ (#27591)
'grapheme_levenshtein' => 'int|false',
// ext/intl/normalizer/normalizer.stub.php — InternalArgInfo still string (missing |false) (#25586)
'normalizer_normalize' => 'string|false',
// ext/intl/normalizer/normalizer.stub.php — absent from InternalArgInfo (#27705)
'normalizer_get_raw_decomposition' => '?string',
// ext/intl/php_intl.stub.php — absent from InternalArgInfo (#25200)
Expand Down Expand Up @@ -973,6 +975,12 @@ public static function stubParamTypeOverride(string $callableLc, int $index): ?s
1 => 'int',
default => null,
},
// ext/intl/normalizer/normalizer.stub.php — string $string, int $form = FORM_C (#25586)
'normalizer_normalize' => match ($index) {
0 => 'string',
1 => 'int',
default => null,
},
// ext/intl/normalizer/normalizer.stub.php — string $string, int $form = FORM_C; absent (#27705)
'normalizer_get_raw_decomposition' => match ($index) {
0 => 'string',
Expand Down
4 changes: 4 additions & 0 deletions lib/BuiltinInternalDefaultValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,10 @@ final class BuiltinInternalDefaultValues
'intlcal_create_instance' => [
0 => ['kind' => 'null'],
],
// php-src ext/intl/normalizer/normalizer.stub.php — int $form = Normalizer::FORM_C (16); int infer → 0 (#25586)
'normalizer_normalize' => [
1 => ['kind' => 'int', 'value' => 16],
],
// php-src ext/intl/normalizer/normalizer.stub.php — int $form = Normalizer::FORM_C (16); int infer → 0 (#27705)
'normalizer_get_raw_decomposition' => [
1 => ['kind' => 'int', 'value' => 16],
Expand Down
3 changes: 3 additions & 0 deletions lib/BuiltinParamNames.php
Original file line number Diff line number Diff line change
Expand Up @@ -2151,6 +2151,9 @@ public static function forFunction(string $name): ?array
// php-src ext/intl/php_intl.stub.php — PHP 8.5+ (#27591)
case 'grapheme_levenshtein':
return ['string1', 'string2', 'insertion_cost=', 'replacement_cost=', 'deletion_cost=', 'locale='];
// php-src ext/intl/normalizer/normalizer.stub.php — InternalArgInfo still input/form string (#25586)
case 'normalizer_normalize':
return ['string', 'form='];
// php-src ext/intl/normalizer/normalizer.stub.php — absent from InternalArgInfo (#27705)
case 'normalizer_get_raw_decomposition':
return ['string', 'form='];
Expand Down
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,7 @@
<file>./test/compliance/CurlUpkeepReflectionVMTest.php</file>
<file>./test/compliance/CurlEscapeUnescapeReflectionVMTest.php</file>
<file>./test/compliance/NormalizerGetRawDecompositionReflectionVMTest.php</file>
<file>./test/compliance/NormalizerNormalizeReflectionVMTest.php</file>
<file>./test/compliance/DatefmtFormatObjectReflectionVMTest.php</file>
<file>./test/compliance/TimezoneAbbreviationsListExcessArgc30681VMTest.php</file>
<file>./test/compliance/TimezoneAbbreviationsListExcessArgc30681JITTest.php</file>
Expand Down
27 changes: 27 additions & 0 deletions test/compliance/NormalizerNormalizeReflectionVMTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

require_once __DIR__.'/../BaseTest.php';

/** VM compliance for normalizer_normalize() Reflection stubs (#25586). */
final class NormalizerNormalizeReflectionVMTest extends BaseTest
{
protected static string $DIR = __DIR__;

public static function providePHPTests(): \Generator
{
$file = 'normalizer_normalize_reflection_25586.phpt';
yield $file => self::parsePHPT(
__DIR__.'/cases/intl/'.$file,
$file
);
}

public function setUp(): void
{
$this->BIN = realpath(__DIR__.'/../../bin/vm.php');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
--TEST--
normalizer_normalize() Reflection + named string/form (#25586)
--SKIPIF--
<?php if (!extension_loaded('intl')) die('skip host php-intl required'); ?>
--FILE--
<?php
$rf = new ReflectionFunction('normalizer_normalize');
echo 'arity=', $rf->getNumberOfParameters(), ' req=', $rf->getNumberOfRequiredParameters(), "\n";
echo 'ret=', $rf->hasReturnType() ? (string) $rf->getReturnType() : '(none)', "\n";
foreach ($rf->getParameters() as $p) {
$t = $p->getType();
echo ' ', ($t ? (string) $t.' ' : ''), '$', $p->getName();
if ($p->isOptional()) {
echo ' OPT';
if ($p->isDefaultValueAvailable()) {
echo '=', json_encode($p->getDefaultValue());
}
} else {
echo ' REQ';
}
echo "\n";
}
$s = "e\u{0301}";
try {
echo 'named_string=', bin2hex(normalizer_normalize(string: $s)), "\n";
} catch (Throwable $e) {
echo 'named_string=', get_class($e), ':', $e->getMessage(), "\n";
}
try {
echo 'named_form=', bin2hex(normalizer_normalize(string: $s, form: Normalizer::FORM_C)), "\n";
} catch (Throwable $e) {
echo 'named_form=', get_class($e), ':', $e->getMessage(), "\n";
}
try {
echo 'positional=', bin2hex(normalizer_normalize($s)), "\n";
} catch (Throwable $e) {
echo 'positional=', get_class($e), ':', $e->getMessage(), "\n";
}
try {
normalizer_normalize(input: $s);
echo "legacy_input accepted\n";
} catch (Throwable $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
arity=2 req=1
ret=string|false
string $string REQ
int $form OPT=16
named_string=c3a9
named_form=c3a9
positional=c3a9
Unknown named parameter $input
57 changes: 57 additions & 0 deletions test/repro/issue_25586_normalizer_normalize_reflection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php
/**
* Repro #25586 — normalizer_normalize Reflection must match
* php-src normalizer.stub.php: (string $string, int $form = FORM_C): string|false
* + named string:/form:; named input: rejected.
*
* Force-registers when host php-intl is absent (Docker #22691).
*
* ./script/docker-exec.sh -- bash -lc 'php test/repro/issue_25586_normalizer_normalize_reflection.php'
*/
require __DIR__ . '/../../vendor/autoload.php';

use PHPCompiler\Runtime;
use PHPCompiler\ext\intl\BuiltinClasses;
use PHPCompiler\ext\intl\IntlExtensionPolicy;
use PHPCompiler\ext\intl\normalizer_normalize;

$runtime = new Runtime();
if (!IntlExtensionPolicy::advertisesNormalizer()) {
BuiltinClasses::registerNormalizer($runtime->vmContext);
$runtime->vmContext->declareFunction(new normalizer_normalize());
}

$code = <<<'PHP'
<?php
$r = new ReflectionFunction('normalizer_normalize');
$params = [];
foreach ($r->getParameters() as $p) {
$t = $p->getType();
$line = $p->getName().':'.($t ? (string) $t : 'none');
if ($p->isOptional()) {
$line .= ' opt';
if ($p->isDefaultValueAvailable()) {
$line .= '='.var_export($p->getDefaultValue(), true);
}
}
$params[] = $line;
}
$rt = $r->getReturnType();
echo 'arity='.$r->getNumberOfParameters().'|'.implode(',', $params).'|'.($rt ? (string) $rt : 'none')."\n";
$s = "e\u{0301}";
try {
echo 'named='.bin2hex(normalizer_normalize(string: $s))."\n";
} catch (Throwable $e) {
echo 'named ERR='.$e->getMessage()."\n";
}
try {
normalizer_normalize(input: $s);
echo "legacy_input accepted\n";
} catch (Throwable $e) {
echo 'legacy='.$e->getMessage()."\n";
}
echo 'positional='.bin2hex(normalizer_normalize($s))."\n";
PHP;

$block = $runtime->parseAndCompile($code, 'issue_25586_normalizer_normalize_reflection.php');
$runtime->run($block);
41 changes: 41 additions & 0 deletions test/unit/BuiltinInternalArgInfoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,47 @@ public function testCurlGetinfoReflectionStubTypes(): void
$this->assertSame(\PHPCompiler\VM\Variable::TYPE_NULL, $dest->type);
}

/** php-src normalizer.stub.php — $string/$form int NFC → string|false; InternalArgInfo still $input/string (#25586). */
public function testNormalizerNormalizeReflectionStubTypes(): void
{
$fn = 'normalizer_normalize';
$this->assertSame('string|false', BuiltinInternalArgInfo::returnTypeLabelForFunction($fn));
$this->assertSame('string', BuiltinInternalArgInfo::stubParamTypeOverride($fn, 0));
$this->assertSame('int', BuiltinInternalArgInfo::stubParamTypeOverride($fn, 1));
$string = BuiltinInternalArgInfo::paramInfoForFunction($fn, 0);
$this->assertNotNull($string);
$this->assertSame('string', $string['type']);
$this->assertFalse($string['isOptional']);
$form = BuiltinInternalArgInfo::paramInfoForFunction($fn, 1);
$this->assertNotNull($form);
$this->assertSame('int', $form['type']);
$this->assertSame('form', $form['name']);
$this->assertTrue($form['isOptional']);
$this->assertSame(['string', 'form='], BuiltinParamNames::forFunction($fn));
$this->assertSame(['string', 'form='], BuiltinParamNames::paramNamesForInternalFunction($fn));
$this->assertSame(0, BuiltinParamNames::lookupNamedParamIndex(
BuiltinParamNames::forFunction($fn),
'string',
$fn
));
$this->assertSame(1, BuiltinParamNames::lookupNamedParamIndex(
BuiltinParamNames::forFunction($fn),
'form',
$fn
));
$this->assertFalse(BuiltinParamNames::lookupNamedParamIndex(
BuiltinParamNames::forFunction($fn),
'input',
$fn
));
$this->assertSame(1, BuiltinParamNames::requiredParamCountForInternalFunction($fn));
$this->assertTrue(BuiltinInternalDefaultValues::isAvailable($fn, 1, $form, false));
$dest = new \PHPCompiler\VM\Variable();
$this->assertTrue(BuiltinInternalDefaultValues::materialize($dest, $fn, 1, $form));
$this->assertSame(\PHPCompiler\VM\Variable::TYPE_INTEGER, $dest->type);
$this->assertSame(16, $dest->toInt());
}

/** php-src normalizer.stub.php — string/form → ?string; absent from InternalArgInfo (#27705). */
public function testNormalizerGetRawDecompositionReflectionStubTypes(): void
{
Expand Down
57 changes: 57 additions & 0 deletions test/unit/VmNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,61 @@ public function test_invalid_form_value_error(): void
$this->expectException(\ValueError::class);
VmNormalizer::normalize('x', 99);
}

/**
* php-src normalizer.stub.php — $string/$form + named string:; input: rejected (#25586).
*
* Force-registers when host php-intl is absent (Docker #22691).
*/
public function test_normalize_reflection_named_string_via_forced_registration(): void
{
$runtime = new Runtime();
if (!\PHPCompiler\ext\intl\IntlExtensionPolicy::advertisesNormalizer()) {
\PHPCompiler\ext\intl\BuiltinClasses::registerNormalizer($runtime->vmContext);
$runtime->vmContext->declareFunction(new \PHPCompiler\ext\intl\normalizer_normalize());
}
$code = <<<'PHP'
<?php
$rf = new ReflectionFunction('normalizer_normalize');
echo 'arity=', $rf->getNumberOfParameters(), ' req=', $rf->getNumberOfRequiredParameters(), "\n";
echo 'ret=', $rf->hasReturnType() ? (string) $rf->getReturnType() : '(none)', "\n";
foreach ($rf->getParameters() as $p) {
$t = $p->getType();
echo ' ', ($t ? (string) $t.' ' : ''), '$', $p->getName();
if ($p->isOptional()) {
echo ' OPT';
if ($p->isDefaultValueAvailable()) {
echo '=', json_encode($p->getDefaultValue());
}
} else {
echo ' REQ';
}
echo "\n";
}
$s = "e\u{0301}";
echo 'named_string=', bin2hex(normalizer_normalize(string: $s)), "\n";
echo 'named_form=', bin2hex(normalizer_normalize(string: $s, form: Normalizer::FORM_C)), "\n";
echo 'positional=', bin2hex(normalizer_normalize($s)), "\n";
try {
normalizer_normalize(input: $s);
echo "legacy_input accepted\n";
} catch (Throwable $e) {
echo $e->getMessage(), "\n";
}
PHP;
$block = $runtime->parseAndCompile($code, 'normalizer_normalize_reflection_25586.php');
ob_start();
$runtime->run($block);
self::assertSame(
"arity=2 req=1\n"
."ret=string|false\n"
." string \$string REQ\n"
." int \$form OPT=16\n"
."named_string=c3a9\n"
."named_form=c3a9\n"
."positional=c3a9\n"
."Unknown named parameter \$input\n",
ob_get_clean()
);
}
}
Loading