diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml new file mode 100644 index 00000000..45e2300e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug-report.yml @@ -0,0 +1,32 @@ +name: Bug report +description: If something isn't working as expected. +title: "" +labels: ["bug"] +body: + - type: textarea + id: description + attributes: + label: Bug report + description: Please describe your problem here + placeholder: Leave a comment + validations: + required: true + + - type: input + id: reproduction + attributes: + label: Code snippet that reproduces the problem + description: | + Please reproduce the issue with the [on-line TypePHP playground](https://typephp-php.github.io/docs/playground), get a unique URL with the Share button and post it here. + placeholder: https://typephp-php.github.io/docs/playground#code= + validations: + required: false + + - type: textarea + id: expected + attributes: + label: Expected output + description: Was the issue reported incorrectly? Or should TypePHP detect an issue with the code but doesn't? + placeholder: Leave a comment + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.yml b/.github/ISSUE_TEMPLATE/2-feature-request.yml new file mode 100644 index 00000000..732ef2cf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-feature-request.yml @@ -0,0 +1,32 @@ +name: Feature request +description: I have a suggestion (and may want to implement it)! +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Have an idea to improve TypePHP? We would love to hear it! + - type: textarea + id: proposal + attributes: + label: Proposed Feature / Syntax + description: Describe the feature or type annotation you would like TypePHP to support. + placeholder: E.g., Support for a new scalar refinement, shape feature, or CLI option... + validations: + required: true + - type: textarea + id: motivation + attributes: + label: Motivation / Use Case + description: Why should this be added to TypePHP? What problem does it solve? + validations: + required: true + - type: textarea + id: example + attributes: + label: Code Example + description: Show a code snippet demonstrating how this feature would look and behave. + render: php + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/3-type-mismatch.yml b/.github/ISSUE_TEMPLATE/3-type-mismatch.yml new file mode 100644 index 00000000..0a01d490 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-type-mismatch.yml @@ -0,0 +1,31 @@ +name: Function signature or stub mismatch +description: Some built-in PHP function or vendor stub expects or returns a different type than reported. +title: "[Type Mismatch]: " +labels: ["types", "stubs"] +body: + - type: markdown + attributes: + value: | + Use this template if TypePHP reports an invalid type on a built-in PHP function, or if a DocBlock contract contradicts real PHP behavior. + - type: input + id: target + attributes: + label: Affected Function, Method, or Class + placeholder: E.g., array_map(), DateTime::format(), or Vendor\Package\Service + validations: + required: true + - type: textarea + id: current-vs-expected + attributes: + label: Current vs Expected Type Contract + description: What does TypePHP currently enforce versus what does PHP actually allow/return? + validations: + required: true + - type: input + id: playground + attributes: + label: Playground Link (Optional) + description: If possible, provide a playground reproduction. + placeholder: https://typephp-php.github.io/docs/playground#code=... + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..c450b231 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Report a security vulnerability + url: https://github.com/typephp-php/typephp/security/policy + about: Please review our security policy for more details. + - name: Support question + url: https://github.com/typephp-php/typephp/discussions/new?category=q-a + about: Please open a new discussion instead. Thank you. \ No newline at end of file diff --git a/src/Internal/Ast/ContractVisitor.php b/src/Internal/Ast/ContractVisitor.php index 8b075050..a628984f 100644 --- a/src/Internal/Ast/ContractVisitor.php +++ b/src/Internal/Ast/ContractVisitor.php @@ -322,6 +322,7 @@ private function enterClassLike(Node\Stmt\Class_|Node\Stmt\Interface_|Node\Stmt\ { $typeName = $this->resolveQualifiedName($node->name); $hasInheritance = true; + $hasRealInheritance = false; $hasPropertyWithDoc = false; $isReadonly = false; $hasTemplates = false; @@ -350,11 +351,15 @@ private function enterClassLike(Node\Stmt\Class_|Node\Stmt\Interface_|Node\Stmt\ $hasTemplates || str_contains($doc->getText(), '@phpstan-') || str_contains($doc->getText(), '@psalm-') + || str_contains($doc->getText(), '@method') + || str_contains($doc->getText(), '@property') ); - $hasInheritance = $hasExtends || $hasImplements || $hasTraits || $hasClassDoc; + $hasRealInheritance = $hasExtends || $hasImplements || $hasTraits; + $hasInheritance = $hasRealInheritance || $hasClassDoc; } elseif ($node instanceof Node\Stmt\Enum_) { - $hasInheritance = $node->implements !== []; + $hasRealInheritance = $node->implements !== []; + $hasInheritance = $hasRealInheritance; } else { $doc = $node->getDocComment(); $hasTemplates = $doc !== null && ( @@ -368,6 +373,7 @@ private function enterClassLike(Node\Stmt\Class_|Node\Stmt\Interface_|Node\Stmt\ 'name' => $typeName, 'isAnonymous' => ($node instanceof Node\Stmt\Class_ && $node->name === null), 'hasInheritance' => $hasInheritance, + 'hasRealInheritance' => $hasRealInheritance, 'hasPropertyWithDoc' => $hasPropertyWithDoc, 'isReadonly' => $isReadonly, 'hasTemplates' => $hasTemplates, diff --git a/src/Internal/Ast/FunctionContractInjector.php b/src/Internal/Ast/FunctionContractInjector.php index 990170d5..3c029cc0 100644 --- a/src/Internal/Ast/FunctionContractInjector.php +++ b/src/Internal/Ast/FunctionContractInjector.php @@ -28,7 +28,7 @@ final class FunctionContractInjector ]; /** - * @param array{hasInheritance?: bool, hasPropertyWithDoc?: bool, isReadonly?: bool, hasTemplates?: bool}|null $classContext + * @param array{hasInheritance?: bool, hasRealInheritance?: bool, hasPropertyWithDoc?: bool, isReadonly?: bool, hasTemplates?: bool}|null $classContext */ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?array $classContext = null): void { @@ -41,6 +41,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ? $docText = $doc !== null ? $doc->getText() : ''; $hasInheritance = $classContext['hasInheritance'] ?? true; + $hasRealInheritance = $classContext['hasRealInheritance'] ?? $hasInheritance; $hasPropertyWithDoc = $classContext['hasPropertyWithDoc'] ?? true; $isReadonlyClass = $classContext['isReadonly'] ?? false; $hasClassTemplates = $classContext['hasTemplates'] ?? false; @@ -80,7 +81,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ? $hasParamOut = $byRefParams !== [] && ($hasParamOutDoc || $hasInheritance); $hasSelfOutDoc = str_contains($docText, 'self-out') || str_contains($docText, 'this-out'); - $hasSelfOut = $isClassMethod && ! $node->isStatic() && ($hasSelfOutDoc || ($hasClassTemplates && $hasInheritance)); + $hasSelfOut = $isClassMethod && ! $node->isStatic() && ($hasSelfOutDoc || ($hasClassTemplates && $hasRealInheritance)); $hasReturnDoc = str_contains($docText, '@return') || str_contains($docText, '@phpstan-return') @@ -275,7 +276,7 @@ private static function resolveThisArg(bool $isClassMethod, Node\Stmt\Function_| private static function isGenerator(Node\Stmt\Function_|Node\Stmt\ClassMethod $node): bool { - $visitor = new class () extends NodeVisitorAbstract { + $visitor = new class() extends NodeVisitorAbstract { public bool $isGen = false; public function enterNode(Node $n): ?int @@ -357,24 +358,30 @@ private static function buildSetupScopeStmt(array $params, Node\Expr $thisArg): $cacheKeyExpr = new Node\Scalar\MagicConst\Method(); + $noParamFetch = new Node\Expr\StaticPropertyFetch( + new Node\Name\FullyQualified('TypePHP\Internal\Checker\ParamChecker'), + 'noParamContractCache' + ); + $noParamFetch->setAttribute('typephp_checked', true); + $noParamCacheCheck = new Node\Expr\BooleanNot( new Node\Expr\Isset_([ new Node\Expr\ArrayDimFetch( - new Node\Expr\StaticPropertyFetch( - new Node\Name\FullyQualified('TypePHP\Internal\Checker\ParamChecker'), - 'noParamContractCache' - ), + $noParamFetch, $cacheKeyExpr ), ]) ); + $hasTemplatesFetch = new Node\Expr\StaticPropertyFetch( + new Node\Name\FullyQualified('TypePHP\Internal\RuntimeTypeChecker'), + 'hasMethodTemplatesCache' + ); + $hasTemplatesFetch->setAttribute('typephp_checked', true); + $hasTemplatesCheck = new Node\Expr\BinaryOp\Coalesce( new Node\Expr\ArrayDimFetch( - new Node\Expr\StaticPropertyFetch( - new Node\Name\FullyQualified('TypePHP\Internal\RuntimeTypeChecker'), - 'hasMethodTemplatesCache' - ), + $hasTemplatesFetch, $cacheKeyExpr ), new Node\Expr\ConstFetch(new Node\Name('false')) @@ -671,10 +678,8 @@ public static function buildWrappedYieldNode(Node\Expr\Yield_ $n, Node\Expr $thi private static function wrapGeneratorReturns(array $stmts, Node\Expr $thisArg): array { $traverser = new NodeTraverser(); - $traverser->addVisitor(new class ($thisArg) extends NodeVisitorAbstract { - public function __construct(private Node\Expr $thisArg) - { - } + $traverser->addVisitor(new class($thisArg) extends NodeVisitorAbstract { + public function __construct(private Node\Expr $thisArg) {} public function enterNode(Node $n): int|Node|null { @@ -733,7 +738,7 @@ private static function wrapNonGeneratorReturns( bool $hasSelfOut = false ): array { $traverser = new NodeTraverser(); - $traverser->addVisitor(new class ($thisArg, $isNativeVoid, $needsReturnVars, $hasReturn, $byRefParams, $hasSelfOut) extends NodeVisitorAbstract { + $traverser->addVisitor(new class($thisArg, $isNativeVoid, $needsReturnVars, $hasReturn, $byRefParams, $hasSelfOut) extends NodeVisitorAbstract { /** * @param array $byRefParams */ @@ -744,8 +749,7 @@ public function __construct( private bool $hasReturn, private array $byRefParams, private bool $hasSelfOut - ) { - } + ) {} public function enterNode(Node $n): int|array|null {