fix(compiler): reject native symbol collisions

pull/43/head
韩天峰 1 month ago
parent f78d19fffd
commit 61388a9bd1
  1. 17
      phpunit/code/function-method-symbol-collision-reversed.php
  2. 30
      phpunit/src/FunctionTest.php
  3. 68
      src/CompilerBase.php
  4. 26
      src/Context/CompilationStateTrait.php
  5. 2
      src/Entity/FunctionDef.php
  6. 2
      src/Parser/UniversalMethodCall.php
  7. 23
      src/Preprocessor.php
  8. 12
      src/Translator.php

@ -0,0 +1,17 @@
<?php
namespace Collision\Worker {
function validate(mixed $validater): mixed
{
return $validater;
}
}
namespace Collision {
class Worker
{
public function validate(array $data = []): void
{
}
}
}

@ -2,19 +2,35 @@
class FunctionTest extends \BaseTest
{
public function testFunctionCallDoesNotResolveCollidingClassMethodSymbol(): void
public function testFunctionAndMethodNativeNameCollisionIsRejected(): void
{
$this->assertFunctionMethodNativeNameCollision('function-method-symbol-collision.php');
}
public function testFunctionAndMethodNativeNameCollisionIsRejectedInReverseOrder(): void
{
$this->assertFunctionMethodNativeNameCollision('function-method-symbol-collision-reversed.php');
}
private function assertFunctionMethodNativeNameCollision(string $filename): void
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$testFile = __DIR__ . '/../code/function-method-symbol-collision.php';
$testFile = __DIR__ . '/../code/' . $filename;
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$cppFile = $compiler->convertFile($testFile);
$cpp = file_get_contents($cppFile);
$this->assertStringContainsString('php_collision__worker__NSE__validate(validater)', $cpp);
$this->assertStringContainsString('php_collision__NSE__worker__validate(', $cpp);
try {
$compiler->prepareFile($testFile);
$this->fail('Expected a native symbol collision');
} catch (\TypePhp\Exception\TestError $error) {
$message = $error->getMessage();
$this->assertStringContainsString('C++ symbol collision', $message);
$this->assertStringContainsString('Collision\\Worker::validate()', $message);
$this->assertStringContainsString('Collision\\Worker\\validate()', $message);
$this->assertStringContainsString('php_collision__worker__validate', $message);
$this->assertStringContainsString('rename one of them', $message);
}
}
public function testReturnRef(): void

@ -196,7 +196,6 @@ class CompilerBase implements PropertyAccessContext
public const string FUNC_MAP = 'func_map';
public const string PROP_MAP = 'property_map';
public const string NAMESPACE_SEPARATOR = '__';
public const string NAMESPACE_END_MARKER = 'NSE';
public const string PREFIX = 'php_';
protected const string MULTI_RETURN_NAMESPACE = 'typephp::detail';
@ -1019,14 +1018,7 @@ class CompilerBase implements PropertyAccessContext
protected function getFunctionName(FunctionLike $v): string
{
if ($this->class !== '') {
return $this->getNativeClassMethodName(
$this->parseIdentifier($v->name),
$this->namespace,
$this->class,
);
}
return $this->getNativeFunctionName($this->parseIdentifier($v->name), $this->namespace);
return $this->getNativeName($this->parseIdentifier($v->name), $this->namespace, $this->class);
}
protected function getFullClassName(): string
@ -1149,29 +1141,6 @@ class CompilerBase implements PropertyAccessContext
return implode(self::NAMESPACE_SEPARATOR, $names);
}
protected function getNativeFunctionName(string $function, string $namespace = ''): string
{
$names = [];
if ($namespace !== '') {
$names[] = $this->escapeNamespace($namespace);
$names[] = self::NAMESPACE_END_MARKER;
}
$names[] = $this->escapeName($function);
return implode(self::NAMESPACE_SEPARATOR, $names);
}
protected function getNativeClassMethodName(string $method, string $namespace, string $class): string
{
$names = [];
if ($namespace !== '') {
$names[] = $this->escapeNamespace($namespace);
}
$names[] = self::NAMESPACE_END_MARKER;
$names[] = $this->escapeClass($class);
$names[] = $this->escapeName($method);
return implode(self::NAMESPACE_SEPARATOR, $names);
}
protected function getClassId(string $className): int
{
if (isset($this->classMap[$className])) {
@ -2393,7 +2362,7 @@ class CompilerBase implements PropertyAccessContext
if ($checkArgs) {
$this->checkNativeCallArgs($expr, $methodDef->functionDef, $expr->args, $classDef->getNamespacedName() . '::' . $method);
}
return $this->getNativeClassMethodName($method, $classDef->namespace, $classDef->name);
return $this->getNativeName($method, $classDef->namespace, $classDef->name);
}
protected function findNativeClassConst(NodeAbstract $expr, string $class, string $const): string|false
@ -2845,33 +2814,17 @@ class CompilerBase implements PropertyAccessContext
// 绝对命名空间的函数
if ($funcName[0] == '\\') {
$funcName = ltrim($funcName, '\\');
$separator = strrpos($funcName, '\\');
if ($separator === false) {
$possibleFunctionNames = [$this->getNativeFunctionName($funcName)];
} else {
$possibleFunctionNames = [$this->getNativeFunctionName(
substr($funcName, $separator + 1),
substr($funcName, 0, $separator),
)];
}
$possibleFunctionNames = [$this->escapeName($funcName)];
} else {
$possibleFunctionNames = [$this->getNativeFunctionName($funcName)];
$possibleFunctionNames = [$this->escapeName($funcName)];
if (isset($this->useAliases[$funcName])) {
$alias = $this->useAliases[$funcName];
$separator = strrpos($alias, '\\');
$possibleFunctionNames[] = $separator === false
? $this->getNativeFunctionName($alias)
: $this->getNativeFunctionName(substr($alias, $separator + 1), substr($alias, 0, $separator));
$possibleFunctionNames[] = $this->escapeName($this->escapeNamespace($this->useAliases[$funcName]));
}
if ($this->namespace) {
$possibleFunctionNames[] = $this->getNativeFunctionName($funcName, $this->namespace);
$possibleFunctionNames[] = $this->escapeNamespace($this->namespace) . self::NAMESPACE_SEPARATOR . $this->escapeName($funcName);
}
if (isset($this->useFunctions[$funcName])) {
$alias = $this->useFunctions[$funcName];
$separator = strrpos($alias, '\\');
$possibleFunctionNames[] = $separator === false
? $this->getNativeFunctionName($alias)
: $this->getNativeFunctionName(substr($alias, $separator + 1), substr($alias, 0, $separator));
$possibleFunctionNames[] = $this->escapeNamespace($this->useFunctions[$funcName]);
}
// 复杂命名空间规则,组合命名空间
// 例子:use foo\bar; bar\fn();
@ -2881,12 +2834,7 @@ class CompilerBase implements PropertyAccessContext
if ($ns1[array_key_last($ns1)] === $ns2[array_key_first($ns2)]) {
$ns = array_merge($ns1, $ns2);
array_splice($ns, array_key_last($ns1) + 1);
$fullName = implode('\\', $ns);
$separator = strrpos($fullName, '\\');
$possibleFunctionNames[] = $this->getNativeFunctionName(
substr($fullName, $separator + 1),
substr($fullName, 0, $separator),
);
$possibleFunctionNames[] = $this->escapeNamespace(implode('\\', $ns));
break;
}
}

@ -106,7 +106,7 @@ trait CompilationStateTrait
protected function addFunction(string $name, FunctionDef $functionDef): void
{
$this->symbols->putFunction($this->escapeNativeFunctionKey($name), $functionDef);
$this->symbols->putFunction($this->escapeFunction($name), $functionDef);
}
/**
@ -114,32 +114,12 @@ trait CompilationStateTrait
*/
protected function hasFunction(string $name): bool
{
return $this->symbols->hasFunction($this->escapeNativeFunctionKey($name));
return $this->symbols->hasFunction($this->escapeFunction($name));
}
protected function getFunction(string $name): FunctionDef
{
return $this->symbols->function($this->escapeNativeFunctionKey($name));
}
/** Normalize PHP name segments without lowercasing the reserved ABI marker. */
private function escapeNativeFunctionKey(string $name): string
{
$placeholder = "\x00";
$leadingPlaceholder = "\x01";
$name = str_replace(
self::NAMESPACE_SEPARATOR . self::NAMESPACE_END_MARKER . self::NAMESPACE_SEPARATOR,
self::NAMESPACE_SEPARATOR . $placeholder . self::NAMESPACE_SEPARATOR,
$name,
);
if (str_starts_with($name, self::NAMESPACE_END_MARKER . self::NAMESPACE_SEPARATOR)) {
$name = $leadingPlaceholder . substr($name, strlen(self::NAMESPACE_END_MARKER));
}
return str_replace(
[$placeholder, $leadingPlaceholder],
[self::NAMESPACE_END_MARKER, self::NAMESPACE_END_MARKER],
$this->escapeFunction($name),
);
return $this->symbols->function($this->escapeFunction($name));
}
protected function addClass(string $name, ClassDef $classDef): void

@ -52,6 +52,8 @@ class FunctionDef
public string $sourceFile = '';
/** First source line of this function definition. */
public int $startLine = 1;
/** PHP-level function or Class::method name used in diagnostics. */
public string $displayName = '';
/**
* @var string 必须是带有命名空间的完整类名

@ -392,7 +392,7 @@ trait UniversalMethodCall
}
$registry[$targetKey][$methodKey] = [
'handler' => 'provider_extension',
'fn' => $this->getNativeClassMethodName($method->name, $provider->namespace, $provider->name),
'fn' => $this->getNativeName($method->name, $provider->namespace, $provider->name),
'class' => $provider->getNamespacedName(false),
'return_type' => $function->returnType,
'min_args' => max(0, $function->argCountRequired - 1),

@ -642,6 +642,24 @@ class Preprocessor extends CompilerBase
$this->function = $this->parseIdentifier($v->name);
$name = $this->getFunctionName($v);
if ($this->hasFunction($name)) {
$existing = $this->getFunction($name);
$currentIsMethod = $this->methodDef !== null;
if ($existing->method !== $currentIsMethod) {
$currentDisplayName = $currentIsMethod
? $this->classDef->getNamespacedName(false) . '::' . $this->function
: ltrim($this->namespace . '\\' . $this->function, '\\');
$existingDisplayName = $existing->displayName !== ''
? $existing->displayName
: $existing->getNamespacedName();
$existingLocation = $existing->sourceFile !== ''
? " (previously declared in {$existing->sourceFile}:{$existing->startLine})"
: '';
$this->fatalError(
$v,
"C++ symbol collision: `{$existingDisplayName}()` and `{$currentDisplayName}()` "
. "both map to `" . self::PREFIX . "{$name}`{$existingLocation}; rename one of them",
);
}
$this->fatalError($v, "Duplicate function `{$name}`");
}
// 禁止重定义内置函数
@ -662,9 +680,12 @@ class Preprocessor extends CompilerBase
}
$functionDef->sourceFile = $this->file;
$functionDef->startLine = $v->getStartLine();
$functionDef->method = $this->methodDef !== null;
$functionDef->displayName = $functionDef->method
? $this->classDef->getNamespacedName(false) . '::' . $functionDef->name
: $functionDef->getNamespacedName();
$this->addFunction($name, $functionDef);
if ($this->methodDef) {
$functionDef->method = true;
$this->methodDef->functionDef = $functionDef;
}
}

@ -2471,7 +2471,7 @@ CODE;
protected function getNativeMethodName(ClassDef $classDef, MethodDef $methodDef): string
{
return $this->getNativeClassMethodName($methodDef->name, $classDef->namespace, $classDef->name);
return $this->getNativeName($methodDef->name, $classDef->namespace, $classDef->name);
}
protected function parseDeclare(mixed $v): void
@ -4543,8 +4543,8 @@ CODE;
$this->checkTraitMethodOverrideCompatibility($classDef, $methodDef, $classMethodName);
$classDef->addMethod($methodDef);
$traitMethodNativeName = $this->getNativeClassMethodName($traitMethodName, $traitDef->namespace, $traitDef->name);
$classMethodNativeName = $this->getNativeClassMethodName($classMethodName, $classDef->namespace, $classDef->name);
$traitMethodNativeName = $this->getNativeName($traitMethodName, $traitDef->namespace, $traitDef->name);
$classMethodNativeName = $this->getNativeName($classMethodName, $classDef->namespace, $classDef->name);
$argList = ['this_'];
if ($methodDef->parentMethodCalls) {
// Bind parent:: to the class that actually composes the trait. This
@ -4780,7 +4780,7 @@ CODE;
{
$name = $this->escapeZendFnName($functionDef->getNamespacedName());
$cppCode = 'ZEND_FUNCTION(' . $name . '){' . PHP_EOL;
$fn = self::PREFIX . $this->getNativeFunctionName($functionDef->name, $functionDef->namespace);
$fn = self::PREFIX . $this->getNativeName($functionDef->name, $functionDef->namespace);
$cppCode .= $this->genWrapperFunctionArgs($fn, $functionDef, $functionDef->getNamespacedName());
return $cppCode;
@ -4792,9 +4792,9 @@ CODE;
$fullName = ltrim($fullName, '\\');
$separator = strrpos($fullName, '\\');
if ($separator === false) {
return self::PREFIX . $this->getNativeFunctionName($fullName);
return self::PREFIX . $this->getNativeName($fullName);
}
return self::PREFIX . $this->getNativeFunctionName(
return self::PREFIX . $this->getNativeName(
substr($fullName, $separator + 1),
substr($fullName, 0, $separator),
);

Loading…
Cancel
Save