TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

1019 lines
38 KiB

<?php
class ClassTest extends \BaseTest
{
public function testZendToArrayDeclarationCannotAcceptParameters(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'Conversion method `ZendToArrayWithParameters::toArray()` must not accept arguments',
);
$this->compile('zend-class-to-array-parameters.php');
}
public function testZendToArrayDeclarationMustReturnArray(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'Conversion method `ZendToArrayWrongReturn::toArray()` must return exactly `array`',
);
$this->compile('zend-class-to-array-return-type.php');
}
public function testKnownZendClassMustDefineToArray(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'Class `ZendWithoutToArray` must define `toArray()` for this conversion',
);
$this->compile('zend-class-to-array-missing.php');
}
public function testKnownZendClassMayResolveToArrayThroughMagicCall(): void
{
$this->compile('zend-class-to-array-magic.php');
}
public function testOrdinaryClassCannotDeclareToAnyKeywordMethod(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'Method name `toAny()` is reserved for a TypePHP keyword method and cannot be declared here',
);
$this->compile('reserved-to-any-method.php');
}
public function testOrdinaryClassCannotDeclareToRefKeywordMethodCaseInsensitively(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'Method name `TOREF()` is reserved for a TypePHP keyword method and cannot be declared here',
);
$this->compile('reserved-to-ref-method.php');
}
public function testInterfaceCannotDeclareToAnyKeywordMethod(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage('Method name `toAny()` is reserved for a TypePHP keyword method');
$this->compile('reserved-keyword-interface-method.php');
}
public function testTraitCannotDeclareToRefKeywordMethod(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage('Method name `toRef()` is reserved for a TypePHP keyword method');
$this->compile('reserved-keyword-trait-method.php');
}
public function testRuntimeAttributesSupportLiteralAndConstantArrays(): void
{
$this->compile('preprocessor/attribute_array_argument.php');
}
public function testRuntimeAttributesSupportNewExpressionArguments(): void
{
$this->compile('preprocessor/attribute_new_expression_argument.php');
}
public function testGlobalConstantAttributesAreForbidden(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Attributes on global constants are not supported by TypePHP');
$this->compile('global-constant-attribute.php');
}
public function testGetterGeneratesPublicMethodsForInstanceProperties(): void
{
$this->compile('getter.php');
}
public function testGetterRejectsStaticProperties(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Getter can only be applied to instance properties');
$this->compile('getter-static-property.php');
}
public function testGetterRejectsNonPropertyTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Getter can only be applied to instance properties');
$this->compile('getter-function.php');
}
public function testCompileTimeAttributeRejectsAliasesOfTheSameAttributeRepeated(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Getter cannot be repeated on the same declaration');
$this->compile('compile-time-attribute-duplicate.php');
}
public function testGetterSupportsReadonlyProperties(): void
{
$this->compile('getter-readonly-property.php');
}
public function testGetterRejectsHookProperties(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Getter cannot be applied to properties with hooks');
$this->compile('getter-hook-property.php');
}
public function testSetterRejectsReadonlyProperties(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Setter cannot be applied to readonly properties');
$this->compile('setter-readonly-property.php');
}
public function testSetterRejectsHookProperties(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Setter cannot be applied to properties with hooks');
$this->compile('setter-hook-property.php');
}
public function testWithRejectsReadonlyPropertiesIncludingReadonlyClasses(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('With cannot be applied to readonly properties');
$this->compile('with-readonly-property.php');
}
public function testWithRejectsHookProperties(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('With cannot be applied to properties with hooks');
$this->compile('with-hook-property.php');
}
public function testGeneratedMethodRejectsDeclaredMethodConflictCaseInsensitively(): void
{
$this->exec('Duplicate method `getName`', 'generated-method-declared-conflict.php');
}
public function testGeneratedMethodConflictDiagnosticPointsToAttributeAndDeclaration(): void
{
try {
$this->compile('generated-method-declared-conflict.php');
$this->fail('Expected generated Getter conflict');
} catch (\TypePhp\Exception\TestError $error) {
$file = realpath(__DIR__ . '/../code/generated-method-declared-conflict.php');
$this->assertNotFalse($file);
$message = $error->getMessage();
$this->assertStringContainsString('compile-time attribute: #[Getter]', $message);
$this->assertStringContainsString('target: property $name', $message);
$this->assertStringContainsString('source: ' . $file . ':5', $message);
$this->assertStringContainsString('conflict source: declaration at ' . $file . ':8', $message);
}
}
public function testGeneratedMethodsRejectEachOtherCaseInsensitively(): void
{
$this->exec('Duplicate method `getName`', 'generated-method-generated-conflict.php');
}
public function testPrinterGeneratedMethodUsesNormalDuplicateMethodValidation(): void
{
$this->exec('Duplicate method `__toString`', 'printer-generated-method-conflict.php');
}
public function testArrayableGeneratedMethodUsesNormalDuplicateMethodValidation(): void
{
$this->exec('Duplicate method `toArray`', 'arrayable-generated-method-conflict.php');
}
public function testGeneratedMethodMayOverrideCompatibleParentMethod(): void
{
$this->compile('generated-method-parent-conflict.php');
}
public function testGeneratedMethodObeysFinalParentMethodRule(): void
{
$this->exec(
'Cannot override final method `GeneratedMethodFinalConflictParent::withName()`',
'generated-method-final-parent-conflict.php'
);
}
public function testCompileTimeGeneratedPropertyMethodsPrinterAndNotNull(): void
{
$this->compile('compile_time_attributes.php');
}
public function testArrayableAndPrinterFieldSelection(): void
{
$this->compile('arrayable.php');
}
public function testArrayableRejectsNonClassTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Arrayable can only be applied to named classes');
$this->compile('arrayable-invalid-target.php');
}
public function testArrayableAcceptsExplicitFieldsWithoutVisibilityFiltering(): void
{
$this->compile('arrayable-explicit-fields.php');
}
public function testArrayableRejectsDynamicFields(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage(
'Arrayable field `missing` must be a declared instance property accessible from the class'
);
$this->compile('arrayable-dynamic-field.php');
}
public function testArrayableAcceptsPublicAndProtectedParentFields(): void
{
$this->compile('arrayable-parent-visible-fields.php');
}
public function testPrinterAcceptsPrivateSelectedFields(): void
{
$this->compile('printer-private-fields.php');
}
public function testPrinterRejectsPrivateParentFields(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage(
'Printer field `secret` must be a declared instance property accessible from the class'
);
$this->compile('printer-parent-private-field.php');
}
public function testPrinterRejectsStaticFields(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage(
'Printer field `shared` must be a declared instance property accessible from the class'
);
$this->compile('printer-static-field.php');
}
public function testPrinterConvertsNonStringFieldsAndArrayablePreservesValues(): void
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$testFile = __DIR__ . '/../code/printer-arrayable-field-types.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$cppFile = $compiler->convertFile($testFile);
$code = file_get_contents($cppFile);
$printerStart = strpos($code, 'php_printerarrayablefieldtypes____tostring');
$arrayableStart = strpos($code, 'php_printerarrayablefieldtypes__toarray');
$this->assertNotFalse($printerStart);
$this->assertNotFalse($arrayableStart);
$printerCode = substr($code, $printerStart, $arrayableStart - $printerStart);
$arrayableCode = substr($code, $arrayableStart);
$this->assertSame(4, substr_count($printerCode, 'php::toString('));
$this->assertStringNotContainsString('php::toString(', $arrayableCode);
}
public function testArrayableRejectsNonArrayFields(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Arrayable $fields must be an array literal');
$this->compile('arrayable-invalid-argument.php');
}
public function testNotNullRejectsNonParameterTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('NotNull can only be applied to function or method parameters');
$this->compile('not-null-invalid-target.php');
}
public function testNotNullRejectsArrowFunctionParameters(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('NotNull is not supported on arrow function parameters');
$this->compile('not-null-arrow-function.php');
}
public function testNotNullWarnsForExplicitlyNullableParameters(): void
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$reporter = new class implements \TypePhp\Diagnostics\DiagnosticReporter {
/** @var list<string> */
public array $warnings = [];
public function fatal(string $message): never
{
throw new \TypePhp\Exception\TestError($message);
}
public function warning(\PhpParser\Node $node, string $file, string $message): void
{
$this->warnings[] = $message;
}
};
$compiler->setDiagnosticReporter($reporter);
$testFile = __DIR__ . '/../code/not-null-nullable-warning.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$this->assertSame([
'NotNull is applied to nullable parameter `$value`',
'NotNull is applied to nullable parameter `$value`',
'NotNull is applied to nullable parameter `$value`',
], $reporter->warnings);
}
public function testParameterValidationUsesFixedSemanticOrder(): void
{
$parser = (new \PhpParser\ParserFactory())->createForHostVersion();
$ast = $parser->parse(file_get_contents(__DIR__ . '/../code/parameter-validation-order.php'));
$traverser = new \PhpParser\NodeTraverser();
$traverser->addVisitor(new \PhpParser\NodeVisitor\NameResolver(null, ['replaceNodes' => false]));
$traverser->addVisitor(new \TypePhp\Transform\Visitor());
$stmts = $traverser->traverse($ast);
$function = $stmts[0];
$this->assertInstanceOf(\PhpParser\Node\Stmt\Function_::class, $function);
$this->assertInstanceOf(\PhpParser\Node\Expr\BinaryOp\Identical::class, $function->stmts[0]->cond);
$this->assertInstanceOf(\PhpParser\Node\Expr\Empty_::class, $function->stmts[1]->cond);
$this->assertInstanceOf(\PhpParser\Node\Expr\BinaryOp\Identical::class, $function->stmts[2]->cond);
$this->assertInstanceOf(\PhpParser\Node\Expr\FuncCall::class, $function->stmts[2]->cond->left);
$this->assertSame('filter_var', strtolower($function->stmts[2]->cond->left->name->toString()));
}
public function testNotEmptyRejectsArrowFunctionParameters(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('NotEmpty is not supported on arrow function parameters');
$this->compile('not-empty-arrow-function.php');
}
public function testValidateRejectsArrowFunctionParameters(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Validate is not supported on arrow function parameters');
$this->compile('validate-arrow-function.php');
}
public function testValidateRejectsSanitizeFilters(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Validate only accepts FILTER_VALIDATE_* filters');
$this->compile('validate-sanitize.php');
}
public function testValidateRejectsProvablyIncompatibleScalarType(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage(
'Validate filter FILTER_VALIDATE_EMAIL is incompatible with parameter `$email` declared as `int`'
);
$this->compile('validate-incompatible-email-int.php');
}
public function testValidateRejectsArrayWithoutArrayMode(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage(
'Validate filter FILTER_VALIDATE_INT is incompatible with parameter `$values` declared as `array`'
);
$this->compile('validate-incompatible-array.php');
}
public function testValidateAllowsCompatibleUnionAndExplicitArrayMode(): void
{
$this->compile('validate-compatible-types.php');
}
public function testValidateRejectsNonParameterTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Validate can only be applied to function or method parameters');
$this->compile('validate-invalid-target.php');
}
public function testValidateUsesCentralDuplicateAttributeValidation(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Validate cannot be repeated on the same declaration');
$this->compile('validate-duplicate.php');
}
public function testCompileTimeAttributeDiagnosticsContainTargetAndBothConflictSources(): void
{
try {
$this->compile('validate-duplicate.php');
$this->fail('Expected duplicate Validate diagnostic');
} catch (\TypePhp\Exception\SyntaxError $error) {
$message = $error->getMessage();
$file = realpath(__DIR__ . '/../code/validate-duplicate.php');
$this->assertNotFalse($file);
$this->assertStringContainsString('target: parameter $value', $message);
$this->assertStringContainsString('source: ' . $file . ':4', $message);
$this->assertStringContainsString(
'conflict source: #[Validate] at ' . $file . ':5',
$message,
);
}
}
public function testLanguageCompileTimeAttributes(): void
{
$this->compile('language_attributes.php');
}
public function testMethodsForSupportsAliasesAndObjectTargets(): void
{
$this->compile('methods-for.php');
}
public function testMethodsForUsesKeywordClassHierarchyAndObjectFallbackPriority(): void
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$testFile = __DIR__ . '/../code/methods-for-inheritance.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$cppFile = $compiler->convertFile($testFile);
$cpp = file_get_contents($cppFile);
$this->assertStringContainsString('php_hierarchykeywordmethods__keywordwins(', $cpp);
$this->assertStringContainsString('php::echo(php_hierarchybase__realwins(child));', $cpp);
$this->assertStringNotContainsString(
'php::echo(php::toString(php_hierarchyobjectmethods__realwins(',
$cpp,
);
$this->assertStringNotContainsString(
'php::echo(php::toString(php_hierarchyobjectmethods__declaredmethod(',
$cpp,
);
$this->assertStringContainsString('php_hierarchybasemethods__inheritedextension(', $cpp);
$this->assertMatchesRegularExpression(
'/php_hierarchychildmethods__nearestextension\([^,\n]+, child\)/',
$cpp,
);
$this->assertMatchesRegularExpression(
'/php_hierarchybasemethods__nearestextension\([^,\n]+, base\)/',
$cpp,
);
$this->assertSame(
3,
substr_count($cpp, 'php::echo(php::toString(php_hierarchyobjectmethods__objectfallback('),
);
}
public function testMethodsForRejectsKeywordAndTargetSpecificNameConflict(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'conflicts with keyword extension method *::inspect()'
);
$this->compile('methods-for-keyword-conflict.php');
}
public function testMethodsForKeywordConflictDoesNotDependOnDeclarationOrder(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'Keyword extension method *::inspect() conflicts with extension method php::str::inspect()'
);
$this->compile('methods-for-keyword-conflict-reversed.php');
}
public function testMethodsForRejectsInterfaceTargets(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'MethodsFor target InvalidMethodsForContract must be a class; interfaces are not supported'
);
$this->compile('methods-for-interface-target.php');
}
public function testHotAndColdFunctionAttributes(): void
{
$this->compile('hot-cold.php');
}
public function testHotAndColdCannotBeCombined(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Hot and Cold cannot be applied to the same function or method');
$this->compile('hot-cold-conflict.php');
}
public function testHotRejectsNonFunctionTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Hot can only be applied to functions or methods');
$this->compile('hot-invalid-target.php');
}
public function testMustUseRejectsDiscardedReturnValue(): void
{
$this->exec('must be used', 'must-use-discard.php');
}
public function testMustUseRejectsDiscardedMethodReturnValue(): void
{
$this->exec('must be used', 'must-use-method-discard.php');
}
public function testOverrideAcceptsParentInterfaceTraitAndNamespaceAliasMatches(): void
{
$this->compile('override-valid.php');
}
public function testOverrideAcceptsParentPropertiesIncludingPromotedAndHookedProperties(): void
{
$this->compile('override-property-valid.php');
}
public function testPropertyOverrideRequiresMatchingParentProperty(): void
{
$this->exec(
'OverridePropertyMissing::$value has #[\\Override] attribute, but no matching parent class property exists',
'override-property-missing.php',
);
}
public function testPropertyOverrideCannotHidePrivateParentProperty(): void
{
$this->exec(
'Declaration of `OverridePropertyPrivateChild::$value` conflicts with private property '
. '`OverridePropertyPrivateParent::$value`; property shadowing across inheritance is not allowed',
'override-property-private-parent.php',
);
}
public function testPropertyOverrideIsRejectedOnInterfaceProperty(): void
{
$this->exec(
'OverridePropertyInterface::$value has #[\\Override] attribute, '
. 'but no matching parent class property exists',
'override-property-interface.php',
);
}
public function testPropertyOverrideOnTraitIsValidatedAtUseSite(): void
{
$this->exec(
'OverridePropertyTraitConsumer::$value has #[\\Override] attribute, '
. 'but no matching parent class property exists',
'override-property-trait-missing.php',
);
}
public function testOverrideRequiresMatchingParentMethod(): void
{
$this->exec(
'OverrideMissing::missing() has #[\\Override] attribute, but no matching parent method exists',
'override-missing.php',
);
}
public function testOverrideNeverMatchesConstructor(): void
{
$this->exec(
'OverrideConstructorChild::__construct() has #[\\Override] attribute, but no matching parent method exists',
'override-constructor.php',
);
}
public function testOverrideDoesNotMatchPrivateParentMethod(): void
{
$this->exec(
'OverridePrivateChild::value() has #[\\Override] attribute, but no matching parent method exists',
'override-attribute-private-parent.php',
);
}
public function testOverrideOnTraitMethodIsValidatedAtUseSite(): void
{
$this->exec(
'OverrideTraitConsumer::missing() has #[\\Override] attribute, but no matching parent method exists',
'override-trait-missing.php',
);
}
public function testOverrideOnRootInterfaceRequiresParentMethod(): void
{
$this->exec(
'OverrideInterfaceMissing::missing() has #[\\Override] attribute, but no matching parent method exists',
'override-interface-missing.php',
);
}
public function testOverrideRejectsNonMethodOrPropertyTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Override can only be applied to methods or properties');
$this->compile('override-invalid-target.php');
}
public function testOverrideRejectsArguments(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Override does not accept arguments');
$this->compile('override-arguments.php');
}
public function testOverrideCannotBeRepeated(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Override cannot be repeated on the same declaration');
$this->compile('override-duplicate.php');
}
public function testConstructorRejectsExistingConstructor(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage(
'Constructor cannot generate InvalidConstructor::__construct(): method is already declared',
);
$this->compile('constructor-existing.php');
}
public function testConstructorRejectsExistingConstructorRegardlessOfParameterOrder(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage(
'Constructor cannot generate ReorderedConstructor::__construct(): method is already declared',
);
$this->compile('constructor-existing-reordered.php');
}
public function testConstructorRejectsStaticProperties(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Constructor can only be applied to instance properties');
$this->compile('constructor-static.php');
}
public function testConstructorCallsParentConstructorWithoutRequiredArguments(): void
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$testFile = __DIR__ . '/../code/constructor-parent-optional.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$cppFile = $compiler->convertFile($testFile);
$this->assertStringContainsString(
'this_.call(get_persistent_method(',
file_get_contents($cppFile),
);
}
public function testConstructorAllowsParentWithoutConstructor(): void
{
$this->compile('constructor-parent-none.php');
}
public function testConstructorRejectsParentConstructorWithRequiredArguments(): void
{
$this->exec(
'parent constructor `ConstructorRequiredParent::__construct()` requires 1 argument(s)',
'constructor-parent-required.php'
);
}
public function testConstructorDoesNotCallPrivateParentConstructor(): void
{
$this->compile('constructor-parent-private.php');
}
public function testConstructorRejectsFinalParentConstructor(): void
{
$this->exec(
'Cannot override final method `ConstructorFinalParent::__construct()`',
'constructor-parent-final.php'
);
}
public function testReAssignThis()
{
$this->exec('Cannot re-assign $this', 're-assign-this.php');
}
public function testAccessProtectedProperty()
{
$this->exec('Cannot access protected property `settings` of class `DevConfig`', 'protected-property.php');
}
public function testCannotWritePrivateSetPropertyOutsideDeclaringClass()
{
$this->exec('Cannot modify private(set) property', 'private-set-property.php');
}
public function testCannotWriteProtectedSetPropertyOutsideClassFamily()
{
$this->exec('Cannot modify protected(set) property', 'protected-set-property.php');
}
public function testCallAbstractParentMethod()
{
$this->exec('Cannot call abstract method `AbsBase::show()`', 'parent-abstract-method.php');
}
public function testNewAbstractClass()
{
$this->exec('abstract class `AbstractBase` cannot be instantiated', 'abstract-class-new.php');
}
public function testOverridePrivateMethod()
{
$this->exec('Cannot override private method `Base::doWork()`', 'override-private-method.php');
}
public function testPromotedAsymmetricPropertyRequiresType(): void
{
$this->exec(
'Property with asymmetric visibility PromotedAsymmetricUntyped::$value must have type',
'promoted-asymmetric-untyped.php',
);
}
public function testPromotedAsymmetricPropertyRejectsWiderSetVisibility(): void
{
$this->exec(
'Visibility of property PromotedAsymmetricWiderSet::$value must not be weaker than set visibility',
'promoted-asymmetric-wider-set.php',
);
}
public function testFinalPromotedPropertyRequiresExplicitVisibility(): void
{
$this->exec(
'Final promoted property must explicitly declare public, protected, or private visibility',
'final-promoted-property-without-visibility.php',
);
}
public function testTraitMayCallProtectedParentMethod()
{
// A protected parent method is reachable via parent:: from a trait,
// matching PHP runtime behaviour.
$this->compile('trait-parent-method-protected.php');
}
public function testCannotAccessPrivateParentMethodFromRegularClass()
{
$this->exec('Cannot access private method `Base::secret()`', 'parent-method-private.php');
}
public function testCannotAccessPrivateParentMethodFromTrait()
{
$this->exec('Cannot access private method `BaseSecret::secret()`', 'trait-parent-method-private.php');
}
public function testComposedTraitMethodCannotShadowPrivateParentMethod()
{
$this->exec(
'Cannot override private method `PrivateMethodParent::execute()`',
'trait-method-shadows-private.php'
);
}
public function testSelfCanBePartOfUnionType()
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$testFile = __DIR__ . '/../code/union_type_self_allowed.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$compiler->convertFile($testFile);
$this->addToAssertionCount(1);
}
public function testParentCanBePartOfUnionType()
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$testFile = __DIR__ . '/../code/union_type_parent_allowed.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$compiler->convertFile($testFile);
$this->addToAssertionCount(1);
}
public function testSelfCannotBePartOfIntersectionType()
{
$this->exec("Type 'self' cannot be part of an intersection type", 'intersection_type_self_not_allowed.php');
}
public function testParentCannotBePartOfIntersectionType()
{
$this->exec("Type 'parent' cannot be part of an intersection type", 'intersection_type_parent_not_allowed.php');
}
public function testStaticCannotBePartOfIntersectionType()
{
$this->exec("Type 'static' cannot be part of an intersection type", 'intersection_type_static_not_allowed.php');
}
public function testConstructorCannotDeclareReturnType()
{
$this->exec('Method `ConstructorReturnType::__construct()` cannot declare a return type', 'constructor-return-type.php');
}
public function testConstructorCannotReturnValue()
{
$this->exec('Method `ConstructorReturnValue::__construct()` cannot return a value', 'constructor-return-value.php');
}
public function testParentConstructorCannotBeUsedAsValue()
{
$this->compile('parent-constructor-used-as-value.php');
}
public function testParentConstructorCannotBeUsedAsArgument()
{
$this->compile('parent-constructor-used-as-argument.php');
}
public function testVoidExpressionCannotBeUsedAsBinaryOperand()
{
$this->compile('void-expression-binary-operand.php');
}
public function testVoidExpressionCannotBeUsedAsCondition()
{
$this->compile('void-expression-condition.php');
}
public function testVoidExpressionCannotBeUsedAsTernaryBranch()
{
$this->compile('void-expression-ternary-branch.php');
}
public function testVoidExpressionCannotBeUsedAsArrayValue()
{
$this->compile('void-expression-array-value.php');
}
public function testVoidExpressionCannotBeUsedAsMatchArm()
{
$this->compile('void-expression-match-arm.php');
}
public function testDestructorCannotDeclareReturnType()
{
$this->exec('Method `DestructorReturnType::__destruct()` cannot declare a return type', 'destructor-return-type.php');
}
public function testCloneReturnTypeMustBeVoid()
{
$this->exec('Method `CloneInvalidReturnType::__clone()` return type must be void when declared', 'clone-invalid-return-type.php');
}
public function testCallMagicMethodCannotBeStatic()
{
$this->exec('Method MagicCallStaticInvalid::__call() cannot be static', 'magic-call-static.php');
}
public function testCallStaticMagicMethodMustBeStatic()
{
$this->exec('Method MagicCallStaticNonStaticInvalid::__callStatic() must be static', 'magic-callstatic-nonstatic.php');
}
public function testToStringMagicMethodCannotTakeArguments()
{
$this->exec('Method MagicToStringArgsInvalid::__toString() must take exactly 0 arguments', 'magic-tostring-args.php');
}
public function testSetStateMagicMethodMustBeStatic()
{
$this->exec('Method MagicSetStateNonStaticInvalid::__set_state() must be static', 'magic-set-state-nonstatic.php');
}
public function testDestructMagicMethodCannotTakeArguments()
{
$this->exec('Method MagicDestructArgsInvalid::__destruct() must take exactly 0 arguments', 'magic-destruct-args.php');
}
public function testGetMagicMethodParameterMustBeString()
{
$this->exec('Method MagicGetParamTypeInvalid::__get() must take string as argument', 'magic-get-param-type.php');
}
public function testMagicMethodMustBePublic()
{
$this->exec('Method MagicGetProtectedInvalid::__get() must have public visibility', 'magic-get-protected.php');
}
public function testClassConstDefaultValue()
{
// 类常量(self:: / 类名:: / 完全限定名::,含继承自内部父类的常量)
// 作为函数/方法默认参数值应当能够在编译期正确解析。
$this->compile('class-const-default-value.php');
}
public function testClassConstantRejectsNonConstantExpression(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Constant expression contains invalid operations');
$this->compile('class-constant-invalid-expression.php');
}
public function testPropertyDefaultRejectsNonConstantExpression(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Constant expression contains invalid operations');
$this->compile('property-default-invalid-expression.php');
}
public function testClassConstantRejectsObjectCast(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Object casts are not supported in this context');
$this->compile('class-constant-object-cast.php');
}
public function testPropertyDefaultRejectsObjectCast(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Object casts are not supported in this context');
$this->compile('property-default-object-cast.php');
}
public function testPropertyDefaultArrayForIntTypeFailsAtCompileTime()
{
$this->exec(
'Cannot use array as default value for property PropertyDefaultArrayForInt::$a of type int',
'property-default-array-for-int.php'
);
}
public function testPropertyDefaultStringForIntTypeFailsAtCompileTime()
{
$this->exec(
'Cannot use string as default value for property PropertyDefaultStringForInt::$a of type int',
'property-default-string-for-int.php'
);
}
public function testPropertyDefaultNullForNonNullableIntFailsAtCompileTime()
{
$this->exec(
'Cannot use null as default value for property PropertyDefaultNullForInt::$a of type int',
'property-default-null-for-int.php'
);
}
public function testPropertyDefaultArrayForObjectTypeFailsAtCompileTime()
{
$this->exec(
'Cannot use array as default value for property PropertyDefaultArrayForObject::$dep of type PropertyDefaultArrayForObjectDep',
'property-default-array-for-object.php'
);
}
public function testValidPropertyDefaultsCompile()
{
// 合法的默认值(含 int→float 协变、nullable、联合类型、mixed、常量)应通过检查。
$this->compile('property-default-valid.php');
}
public function testTrueDefaultForFalsePropertyFailsAtCompileTime()
{
$this->exec(
'Cannot use true as default value for property PropertyDefaultTrueForFalse::$value of type false',
'property-default-true-for-false.php'
);
}
public function testFalseDefaultForTruePropertyFailsAtCompileTime()
{
$this->exec(
'Cannot use false as default value for property PropertyDefaultFalseForTrue::$value of type true',
'property-default-false-for-true.php'
);
}
public function testClassConstantPropertyDefaultTypeFailsAtCompileTime()
{
$this->exec(
'Cannot use string as default value for property PropertyDefaultClassConstType::$value of type int',
'property-default-class-const-type.php'
);
}
public function testExpressionPropertyDefaultTypeFailsAtCompileTime()
{
$this->exec(
'Cannot use float as default value for property PropertyDefaultExpressionType::$value of type int',
'property-default-expression-type.php'
);
}
}