diff --git a/phpunit/code/function-method-symbol-collision-reversed.php b/phpunit/code/function-method-symbol-collision-reversed.php new file mode 100644 index 00000000..2c40252a --- /dev/null +++ b/phpunit/code/function-method-symbol-collision-reversed.php @@ -0,0 +1,17 @@ +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 diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 96a301a3..2663aadf 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -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; } } diff --git a/src/Context/CompilationStateTrait.php b/src/Context/CompilationStateTrait.php index b02614c7..f6156e80 100644 --- a/src/Context/CompilationStateTrait.php +++ b/src/Context/CompilationStateTrait.php @@ -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 diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index 1b9f344c..4457cdaa 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -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 必须是带有命名空间的完整类名 diff --git a/src/Parser/UniversalMethodCall.php b/src/Parser/UniversalMethodCall.php index 11c7c3f2..967c5886 100644 --- a/src/Parser/UniversalMethodCall.php +++ b/src/Parser/UniversalMethodCall.php @@ -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), diff --git a/src/Preprocessor.php b/src/Preprocessor.php index a3ace405..07125648 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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; } } diff --git a/src/Translator.php b/src/Translator.php index b1f1c81b..861279a9 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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), );