feat(compiler): add native class export validation for library stubs

- Prevent native classes from being exported through library stubs with proper error messages
- Add test cases to verify rejection of exported native classes in library builds
- Validate that NoExport native classes are omitted from library stub generation
- Implement check for native class usage in stub files with appropriate error handling
- Update hot path codegen to use appendValue method instead of append
- Add comprehensive test coverage for native class validation scenarios
master
韩天峰 2 days ago
parent 0a97daa663
commit 2471ed1db7
  1. 50
      phpunit/src/CompilerBaseApiTest.php
  2. 2
      phpunit/src/HotPathCodegenTest.php
  3. 9
      phpunit/src/NativeClass/NativeClassValidationTest.php
  4. 21
      src/Generator/LibraryImportStubGenerator.php
  5. 18
      src/Preprocessor.php

@ -1415,6 +1415,56 @@ YAML);
$this->assertSame(['prime2'], $consumer->getLinkLibs()); $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 public function testNoExportFollowsPhpNamespaceResolution(): void
{ {
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB); $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB);

@ -9,7 +9,7 @@ final class HotPathCodegenTest extends \BaseTest
$code = $this->compileFixture(); $code = $this->compileFixture();
self::assertStringContainsString('items.item(0L, true) = value;', $code); 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) += value;', $code);
self::assertStringContainsString('items.item(0L, true) += other.get(0L);', $code); self::assertStringContainsString('items.item(0L, true) += other.get(0L);', $code);
self::assertStringContainsString('items.item(2L, true) = other.get(0L);', $code); self::assertStringContainsString('items.item(2L, true) = other.get(0L);', $code);

@ -98,6 +98,15 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-anonymous.php'); $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 public function testRejectsUntypedProperty(): void
{ {
$this->expectException(TestError::class); $this->expectException(TestError::class);

@ -14,6 +14,7 @@ use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Parser; use PhpParser\Parser;
use PhpParser\PrettyPrinter; use PhpParser\PrettyPrinter;
use TypePhp\Exception\SyntaxError;
use TypePhp\Transform\CompileTimeAttribute; use TypePhp\Transform\CompileTimeAttribute;
use TypePhp\Transform\CompileTimeAttributeRegistry; use TypePhp\Transform\CompileTimeAttributeRegistry;
@ -103,6 +104,14 @@ final class LibraryImportStubGenerator
if ($this->hasNoExportAttribute($stmt)) { if ($this->hasNoExportAttribute($stmt)) {
return null; return null;
} }
if ($stmt instanceof Node\Stmt\Class_ && $this->hasNativeAttribute($stmt)) {
$name = isset($stmt->namespacedName)
? $stmt->namespacedName->toString()
: ($stmt->name?->toString() ?? '<anonymous>');
throw new SyntaxError(
"Native class `{$name}` cannot be exported through a library stub; mark it with #[NoExport]",
);
}
$comments = array_filter( $comments = array_filter(
$stmt->getComments(), $stmt->getComments(),
@ -163,6 +172,18 @@ final class LibraryImportStubGenerator
return null; 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 private function hasNoExportAttribute(Node $node): bool
{ {
if (!property_exists($node, 'attrGroups')) { if (!property_exists($node, 'attrGroups')) {

@ -954,6 +954,24 @@ class Preprocessor extends CompilerBase
$this->classDef = new ClassDef($this->class, $flags, $this->namespace); $this->classDef = new ClassDef($this->class, $flags, $this->namespace);
$this->classDef->nativeObject = NativeClassAttributeLowering::isNative($class); $this->classDef->nativeObject = NativeClassAttributeLowering::isNative($class);
$this->classDef->exported = !$this->hasNoExportAttribute($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->classDef->methodsForTarget = $this->parseMethodsForTarget($class);
$this->addClass($fullClassName, $this->classDef); $this->addClass($fullClassName, $this->classDef);

Loading…
Cancel
Save