diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 1af04faa..34f4bbbe 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -1415,6 +1415,56 @@ YAML); $this->assertSame(['prime2'], $consumer->getLinkLibs()); } + public function testLibraryBuildRejectsExportedNativeClass(): void + { + global $translator; + $translator = $this->compiler; + $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB); + + $file = $this->fixturePath('library_exported_native.php'); + $this->compiler->addFiles([$file]); + + $this->expectException(TestError::class); + $this->expectExceptionMessage( + 'Native class `LibraryExportedNative` cannot be exported through a library stub; mark it with #[NoExport]', + ); + $this->compiler->prepareFile($file); + } + + public function testNoExportNativeClassIsOmittedFromLibraryStub(): void + { + global $translator; + $translator = $this->compiler; + $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB); + $this->setPropertyValue('outputDir', $this->testDir); + $this->compiler->setTargetName('hidden_native'); + + $file = $this->fixturePath('library_hidden_native.php'); + $this->compiler->addFiles([$file]); + $this->compiler->prepareFile($file); + $this->compiler->convertFile($file); + + $stub = file_get_contents($this->compiler->genLibraryImportStub([$file])); + $this->assertStringContainsString('function library_visible_value(): int', $stub); + $this->assertStringNotContainsString('LibraryHiddenNative', $stub); + $this->assertStringNotContainsString('#[Native]', $stub); + } + + public function testLibraryStubGeneratorRejectsExportedNativeClassWithoutPrepare(): void + { + $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB); + $this->setPropertyValue('outputDir', $this->testDir); + $this->compiler->setTargetName('exported_native'); + + $this->expectException(\TypePhp\Exception\SyntaxError::class); + $this->expectExceptionMessage( + 'Native class `LibraryExportedNative` cannot be exported through a library stub; mark it with #[NoExport]', + ); + $this->compiler->genLibraryImportStub([ + $this->fixturePath('library_exported_native.php'), + ]); + } + public function testNoExportFollowsPhpNamespaceResolution(): void { $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB); diff --git a/phpunit/src/HotPathCodegenTest.php b/phpunit/src/HotPathCodegenTest.php index 97e13f3a..80217461 100644 --- a/phpunit/src/HotPathCodegenTest.php +++ b/phpunit/src/HotPathCodegenTest.php @@ -9,7 +9,7 @@ final class HotPathCodegenTest extends \BaseTest $code = $this->compileFixture(); self::assertStringContainsString('items.item(0L, true) = value;', $code); - self::assertStringContainsString('items.append(value);', $code); + self::assertStringContainsString('items.appendValue(value);', $code); self::assertStringContainsString('items.item(0L, true) += value;', $code); self::assertStringContainsString('items.item(0L, true) += other.get(0L);', $code); self::assertStringContainsString('items.item(2L, true) = other.get(0L);', $code); diff --git a/phpunit/src/NativeClass/NativeClassValidationTest.php b/phpunit/src/NativeClass/NativeClassValidationTest.php index c7f7a730..8d225f6e 100644 --- a/phpunit/src/NativeClass/NativeClassValidationTest.php +++ b/phpunit/src/NativeClass/NativeClassValidationTest.php @@ -98,6 +98,15 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-anonymous.php'); } + public function testRejectsNativeClassDeclaredInStubFile(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage( + '#[Native] cannot be used in .stub.php; Native class layout must be owned by the TypePHP compiler', + ); + $this->compile('native-class-stub.stub.php'); + } + public function testRejectsUntypedProperty(): void { $this->expectException(TestError::class); diff --git a/src/Generator/LibraryImportStubGenerator.php b/src/Generator/LibraryImportStubGenerator.php index 2b793b16..5ae11a15 100644 --- a/src/Generator/LibraryImportStubGenerator.php +++ b/src/Generator/LibraryImportStubGenerator.php @@ -14,6 +14,7 @@ use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; use PhpParser\Parser; use PhpParser\PrettyPrinter; +use TypePhp\Exception\SyntaxError; use TypePhp\Transform\CompileTimeAttribute; use TypePhp\Transform\CompileTimeAttributeRegistry; @@ -103,6 +104,14 @@ final class LibraryImportStubGenerator if ($this->hasNoExportAttribute($stmt)) { return null; } + if ($stmt instanceof Node\Stmt\Class_ && $this->hasNativeAttribute($stmt)) { + $name = isset($stmt->namespacedName) + ? $stmt->namespacedName->toString() + : ($stmt->name?->toString() ?? ''); + throw new SyntaxError( + "Native class `{$name}` cannot be exported through a library stub; mark it with #[NoExport]", + ); + } $comments = array_filter( $stmt->getComments(), @@ -163,6 +172,18 @@ final class LibraryImportStubGenerator return null; } + private function hasNativeAttribute(Node\Stmt\Class_ $class): bool + { + foreach ($class->attrGroups as $group) { + foreach ($group->attrs as $attribute) { + if (CompileTimeAttribute::is($attribute, 'Native')) { + return true; + } + } + } + return false; + } + private function hasNoExportAttribute(Node $node): bool { if (!property_exists($node, 'attrGroups')) { diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 516a900b..8bab8059 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -954,6 +954,24 @@ class Preprocessor extends CompilerBase $this->classDef = new ClassDef($this->class, $flags, $this->namespace); $this->classDef->nativeObject = NativeClassAttributeLowering::isNative($class); $this->classDef->exported = !$this->hasNoExportAttribute($class); + if ($this->classDef->nativeObject && $this->stubFile) { + $this->fatalCompileTimeAttribute( + $class, + 'Native', + '#[Native] cannot be used in .stub.php; Native class layout must be owned by the TypePHP compiler', + ); + } + if ($this->classDef->nativeObject + && $this->classDef->exported + && $this->isBuildModeLib() + && !$this->isWasiTarget() + ) { + $this->fatalCompileTimeAttribute( + $class, + 'Native', + "Native class `{$fullClassName}` cannot be exported through a library stub; mark it with #[NoExport]", + ); + } $this->classDef->methodsForTarget = $this->parseMethodsForTarget($class); $this->addClass($fullClassName, $this->classDef);