Skip to content
Open
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
3 changes: 3 additions & 0 deletions phpunit/code/function-import-policy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php
use function exec as SAFE;
function importedPolicyCall(): void { safe('not-executed'); }
14 changes: 14 additions & 0 deletions phpunit/code/function-import-resolution.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
namespace { function route(): string { return 'global'; } }
namespace AliasLibrary { function route(): string { return 'import'; } }
namespace AliasConsumer {
use function AliasLibrary\route as ROUTE;
use function AliasLibrary\route as GET_CALLED_CLASS;
use function strlen as SIZE;
use function AliasLibrary\route as extract;
use function AliasLibrary\{route as GROUPED};
function exercise(): void {
echo route(), ':', RoUtE(), ':', get_called_class(), ':', sIzE('abc'), ':', grouped(), ':', extract(), ':', \route(), "\n";
}
}
namespace { function main(): void { \AliasConsumer\exercise(); } }
22 changes: 22 additions & 0 deletions phpunit/src/FunctionImportResolutionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

use TypePhp\CompilerTest;

final class FunctionImportResolutionTest extends BaseTest
{
public function testImportedFunctionsTakePrecedenceRegardlessOfAliasCase(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/function-import-resolution.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));
preg_match('/void php_aliasconsumer__exercise\(\) \{(.*?)\n\}/s', $code, $match);
self::assertCount(2, $match);
self::assertSame(5, substr_count($match[1], 'php_aliaslibrary__route('));
self::assertSame(1, substr_count($match[1], 'php_route('));
self::assertStringNotContainsString('php::call(', $match[1]);
}
}
28 changes: 28 additions & 0 deletions phpunit/src/NanoCapabilityPolicyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,34 @@ public function forgetBuildTimeFunction(string $name): void

final class NanoCapabilityPolicyTest extends BaseTest
{
public function testFunctionImportCannotBypassNanoPolicy(): void
{
$this->assertImportedFunctionRejected(false);
}

public function testFunctionImportCannotBypassWasiPolicy(): void
{
$this->assertImportedFunctionRejected(true);
}

private function assertImportedFunctionRejected(bool $wasi): void
{
global $translator;
$compiler = new NanoCapabilityPolicyCompiler(TYPEPHP_ROOT_PATH);
if ($wasi) {
$compiler->enableWasiForTest();
} else {
$compiler->enableNanoForTest();
}
$translator = $compiler;
$source = __DIR__ . '/../code/function-import-policy.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$this->expectException(TestError::class);
$this->expectExceptionMessage('Function `exec` is not supported');
$compiler->convertFile($source);
}

public function testRejectsForbiddenDirectCallMissingFromBuildTimePhp(): void
{
global $translator;
Expand Down
9 changes: 6 additions & 3 deletions src/CompilerBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -3647,14 +3647,17 @@ protected function findNativeFunction(string $funcName): string|false
$possibleFunctionNames = [
$this->escapeName($this->getNamespacedClassName($funcName)),
];
} elseif (isset($this->useFunctions[strtolower($funcName)])) {
// An explicit function import selects one target; it must not
// fall back to a same-named compiled global function.
$possibleFunctionNames = [
$this->escapeNamespace($this->useFunctions[strtolower($funcName)]),
];
} else {
$possibleFunctionNames = [$this->escapeName($funcName)];
if ($this->namespace) {
$possibleFunctionNames[] = $this->escapeNamespace($this->namespace) . self::NAMESPACE_SEPARATOR . $this->escapeName($funcName);
}
if (isset($this->useFunctions[$funcName])) {
$possibleFunctionNames[] = $this->escapeNamespace($this->useFunctions[$funcName]);
}
}

foreach ($possibleFunctionNames as $nativeFunc) {
Expand Down
2 changes: 1 addition & 1 deletion src/Parser/FunctionCallTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ protected function parseFuncCall(Expr\FuncCall $expr): string
$name = '';
} elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') {
$name = $this->parseIdentifier($expr->name);
$globalName = ltrim($name, '\\');
$globalName = strtolower(ltrim($this->getNamespacedFuncName($name), '\\'));
$this->compilationStatistics->record(
CompilationStatistics::FUNCTIONS,
strtolower($globalName),
Expand Down
2 changes: 1 addition & 1 deletion src/Resolver/DeclarationSymbolTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ protected function parseUse(Node\Stmt\Use_ $v2): void
$type = $use->type !== Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $v2->type;
$alias = $this->registerUseImportAlias($use, $type, $id);
if ($type === Node\Stmt\Use_::TYPE_FUNCTION) {
$this->useFunctions[$alias] = $id;
$this->useFunctions[strtolower($alias)] = $id;
} elseif ($type === Node\Stmt\Use_::TYPE_CONSTANT) {
// $id is already the fully qualified constant name. Splitting
// and re-joining it on `\` corrupted single-segment imports:
Expand Down
5 changes: 3 additions & 2 deletions src/Resolver/NameResolutionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,9 @@ public function getNamespacedFuncName(string $funcName): string
if ($funcName[0] == '\\') {
return ltrim($funcName, '\\');
}
if (isset($this->useFunctions[$funcName])) {
return $this->useFunctions[$funcName];
$alias = strtolower($funcName);
if (isset($this->useFunctions[$alias])) {
return $this->useFunctions[$alias];
}
return $funcName;
}
Expand Down
20 changes: 20 additions & 0 deletions tests/compiler/namespace/function-import-resolution.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
Function imports are case-insensitive and take precedence over global functions
--FILE--
<?php
namespace { function route(): string { return 'global'; } }
namespace AliasLibrary { function route(): string { return 'import'; } }
namespace AliasConsumer {
use function AliasLibrary\route as ROUTE;
use function AliasLibrary\route as GET_CALLED_CLASS;
use function strlen as SIZE;
use function AliasLibrary\route as extract;
use function AliasLibrary\{route as GROUPED};
function exercise(): void {
echo route(), ':', RoUtE(), ':', get_called_class(), ':', sIzE('abc'), ':', grouped(), ':', extract(), ':', \route(), "\n";
}
}
namespace { function main(): void { \AliasConsumer\exercise(); } }
?>
--EXPECT--
import:import:import:3:import:import:global
Loading