diff --git a/README.md b/README.md
index ab97366..a52c340 100644
--- a/README.md
+++ b/README.md
@@ -14,9 +14,9 @@
-------
+---
-TypePHP is a transparent, pure-PHP runtime type checker. You don't have to refactor a single line of your codebase, setup complex build toolchains, or compile C-extensions and simply run your existing code, and TypePHP will enforce your extended PHPDoc contracts (generics, array shapes, `key-of`/`value-of` extractions, and scalar refinements) dynamically at runtime.
+TypePHP is a transparent, pure-PHP runtime type checker. You don't have to refactor a single line of your codebase, set up complex build toolchains, or compile C-extensions. Simply run your existing code, and TypePHP will enforce your extended PHPDoc contracts (generics, array shapes, `key-of`/`value-of` extractions, and scalar refinements) dynamically at runtime.
**[Read the full TypePHP documentation »](https://typephp-php.github.io/typephp/)**
@@ -29,15 +29,17 @@ All the documentation lives on the [typephp-php.github.io/typephp website](https
* [Getting Started & Installation Guide](https://typephp-php.github.io/typephp/getting-started/installation)
* [Quick Start Guide](https://typephp-php.github.io/typephp/getting-started/quick-start)
-* [Architecture: How It Works](https://typephp-php.github.io/typephp/architecture/how-it-works)
-* [Core Concepts: Function Contracts](https://typephp-php.github.io/typephp/core-concepts/function-contracts)
-* [Core Concepts: Generics & Bounds](https://typephp-php.github.io/typephp/core-concepts/generics-and-bounds)
+* [Configuration Guide](https://typephp-php.github.io/typephp/getting-started/configuration)
+* [CLI Commands Reference](https://typephp-php.github.io/typephp/getting-started/cli-commands)
+* [Enforcement Boundaries: Function Contracts](https://typephp-php.github.io/typephp/core-concepts/function-contracts)
+* [Runtime Generics & Bounds](https://typephp-php.github.io/typephp/generics/generics-and-bounds)
* [Supported Types: Arrays & Shapes](https://typephp-php.github.io/typephp/supported-types/arrays-and-shapes)
-* [Troubleshooting & FAQ](https://typephp-php.github.io/typephp/advanced/troubleshooting)
+* [Architecture: How It Works](https://typephp-php.github.io/typephp/advanced/how-it-works)
+* [Troubleshooting & FAQ](https://typephp-php.github.io/typephp/troubleshooting)
## Inspiration
-TypePHP is conceptually inspired by Python's [Beartype](https://github.com/beartype/beartype), but bringing transparent runtime type enforcement for type annotations to the PHP ecosystem without any decorators or attributes.
+TypePHP is conceptually inspired by Python's [Beartype](https://github.com/beartype/beartype), bringing transparent runtime type enforcement for type annotations to the PHP ecosystem without any decorators or attributes.
## Sponsors
diff --git a/src/Command/CommandRunner.php b/src/Command/CommandRunner.php
index e7ab1fb..a9091ca 100644
--- a/src/Command/CommandRunner.php
+++ b/src/Command/CommandRunner.php
@@ -6,6 +6,14 @@
final class CommandRunner
{
+ private const KNOWN_COMMANDS = [
+ 'config:init',
+ 'cache:clear',
+ 'cache:warm',
+ 'cache:rebuild',
+ 'help',
+ ];
+
/**
* Parses CLI arguments and routes execution to the corresponding command class.
*
@@ -15,28 +23,51 @@ final class CommandRunner
*/
public static function run(array $args, $outputStream = STDOUT, $errorStream = STDERR): int
{
- $showHelp = \in_array('help', $args, true) || \in_array('typephp:help', $args, true) || \in_array('--help', $args, true) || \in_array('-h', $args, true);
+ $c = [CliFormatter::class, 'color'];
+
+ $showHelp = \in_array('help', $args, true)
+ || \in_array('typephp:help', $args, true)
+ || \in_array('--help', $args, true)
+ || \in_array('-h', $args, true)
+ || $args === [];
- if ($showHelp || $args === []) {
+ if ($showHelp) {
return (new HelpCommand())->execute($args, $outputStream, $errorStream);
}
- if (\in_array('config:init', $args, true) || \in_array('init', $args, true)) {
+ $firstArg = $args[0] ?? '';
+
+ if ($firstArg === 'config:init' || $firstArg === 'init') {
return (new ConfigInitCommand())->execute($args, $outputStream, $errorStream);
}
- if (\in_array('cache:rebuild', $args, true)) {
+ if ($firstArg === 'cache:rebuild') {
return (new CacheRebuildCommand())->execute($args, $outputStream, $errorStream);
}
- if (\in_array('cache:clear', $args, true)) {
+ if ($firstArg === 'cache:clear') {
return (new CacheClearCommand())->execute($args, $outputStream, $errorStream);
}
- if (\in_array('cache:warm', $args, true)) {
+ if ($firstArg === 'cache:warm') {
return (new CacheWarmCommand())->execute($args, $outputStream, $errorStream);
}
+ $hasFileExtension = str_contains(basename($firstArg), '.');
+ $isFileTarget = file_exists($firstArg) || $hasFileExtension;
+
+ if (! $isFileTarget) {
+ fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n");
+ fwrite($errorStream, ' ' . $c('✗', 'red') . ' Command ' . $c('"' . $firstArg . '"', 'bold') . " is not defined.\n\n");
+ fwrite($errorStream, ' ' . $c('Did you mean one of these?', 'yellow') . "\n");
+ foreach (self::KNOWN_COMMANDS as $cmd) {
+ fwrite($errorStream, ' ' . $c('•', 'cyan') . ' ' . $cmd . "\n");
+ }
+ fwrite($errorStream, "\n");
+
+ return 1;
+ }
+
return (new RunCommand())->execute($args, $outputStream, $errorStream);
}
}
diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php
index a576643..67bc72c 100644
--- a/src/Command/RunCommand.php
+++ b/src/Command/RunCommand.php
@@ -11,6 +11,8 @@
*/
final class RunCommand implements CommandInterface
{
+ private const VALID_PHP_EXTENSIONS = ['php', 'phtml', 'php5', 'php7', 'php8', 'phps'];
+
public function execute(array $args, $outputStream = STDOUT, $errorStream = STDERR): int
{
$c = [CliFormatter::class, 'color'];
@@ -21,6 +23,15 @@ public function execute(array $args, $outputStream = STDOUT, $errorStream = STDE
foreach ($args as $arg) {
if (! str_starts_with($arg, '--') && ! str_starts_with($arg, '-')) {
$givenTargetCandidate = $arg;
+ $ext = strtolower(pathinfo($arg, PATHINFO_EXTENSION));
+
+ if ($ext !== '' && ! \in_array($ext, self::VALID_PHP_EXTENSIONS, true)) {
+ fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n");
+ fwrite($errorStream, ' ' . $c('✗', 'red') . ' Target file ' . $c('"' . $arg . '"', 'bold') . " is not a PHP script file. TypePHP can only execute PHP files.\n\n");
+
+ return 1;
+ }
+
if (file_exists($arg)) {
$target = $arg;
}
@@ -31,7 +42,7 @@ public function execute(array $args, $outputStream = STDOUT, $errorStream = STDE
if ($givenTargetCandidate !== null && $target === null) {
fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n");
- fwrite($errorStream, ' ' . $c('✗', 'red') . ' Target file ' . $c('"' . $givenTargetCandidate . '"', 'bold') . " does not exist or is not readable.\n\n");
+ fwrite($errorStream, ' ' . $c('✗', 'red') . ' Target script file ' . $c('"' . $givenTargetCandidate . '"', 'bold') . " does not exist or is not readable.\n\n");
return 1;
}
diff --git a/src/Internal/ContractVisitor.php b/src/Internal/ContractVisitor.php
index bc9c8f1..f80cf8b 100644
--- a/src/Internal/ContractVisitor.php
+++ b/src/Internal/ContractVisitor.php
@@ -5,6 +5,7 @@
namespace TypePHP\Internal;
use PhpParser\Node;
+use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use TypePHP\Internal\Visitor\FunctionContractInjector;
use TypePHP\Internal\Visitor\NodeBuilder;
@@ -26,9 +27,9 @@ public function __construct()
/**
* Traverses and transforms AST nodes during entry.
*
- * @return array|null
+ * @return array|int|null
*/
- public function enterNode(Node $node): array|null
+ public function enterNode(Node $node): array|int|null
{
if ($node instanceof Node\Stmt\Function_
|| $node instanceof Node\Stmt\ClassMethod
@@ -47,6 +48,15 @@ public function enterNode(Node $node): array|null
}
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
+ $doc = $node->getDocComment();
+ if ($doc !== null) {
+ $docText = $doc->getText();
+ $shouldRespectIgnore = (bool) (Config::get()['respect_ignore_tags'] ?? true);
+ if ($shouldRespectIgnore && (str_contains($docText, '@typephp-ignore') || str_contains($docText, '@typephp-disable'))) {
+ return NodeTraverser::DONT_TRAVERSE_CHILDREN;
+ }
+ }
+
FunctionContractInjector::inject($node);
return null;
diff --git a/src/Internal/DocblockNormalizer.php b/src/Internal/DocblockNormalizer.php
index d391f2b..f274f0b 100644
--- a/src/Internal/DocblockNormalizer.php
+++ b/src/Internal/DocblockNormalizer.php
@@ -28,6 +28,8 @@ final class DocblockNormalizer
*/
public static function normalize(string $doc): string
{
+ $doc = preg_replace('/(\\\\?[a-zA-Z_\x80-\xff][\\\\a-zA-Z0-9_\x80-\xff]*::[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*(\??:)/', '"$1"$2', $doc) ?? $doc;
+
if (! str_contains($doc, '{')) {
return $doc;
}
diff --git a/src/Internal/Visitor/PropertyHookInjector.php b/src/Internal/Visitor/PropertyHookInjector.php
index 65e26d3..0b843c6 100644
--- a/src/Internal/Visitor/PropertyHookInjector.php
+++ b/src/Internal/Visitor/PropertyHookInjector.php
@@ -16,7 +16,7 @@ final class PropertyHookInjector
{
public static function process(Node\Stmt\Property $node): void
{
- if ($node->hooks === []) {
+ if (! isset($node->hooks) || ! \is_array($node->hooks) || $node->hooks === []) {
return;
}
@@ -76,15 +76,15 @@ public function __construct(private string $propertyName)
{
}
- public function enterNode(Node $n): int|null
+ public function enterNode(Node $node): int|null
{
- if ($n instanceof Node\Expr\Closure || $n instanceof Node\Expr\ArrowFunction || $n instanceof Node\Stmt\Function_ || $n instanceof Node\Stmt\ClassMethod) {
+ if ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction || $node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
}
- if ($n instanceof Node\Stmt\Return_ && $n->expr !== null) {
- $checkCall = NodeBuilder::createPropertyCheckCall($n->expr, new Node\Expr\Variable('this'), $this->propertyName);
- $n->expr = NodeBuilder::createTernaryThrowExpr($checkCall);
+ if ($node instanceof Node\Stmt\Return_ && $node->expr !== null) {
+ $checkCall = NodeBuilder::createPropertyCheckCall($node->expr, new Node\Expr\Variable('this'), $this->propertyName);
+ $node->expr = NodeBuilder::createTernaryThrowExpr($checkCall);
}
return null;
diff --git a/src/Resolver/SpecialTypeResolver.php b/src/Resolver/SpecialTypeResolver.php
index 4a243f1..8da48d2 100644
--- a/src/Resolver/SpecialTypeResolver.php
+++ b/src/Resolver/SpecialTypeResolver.php
@@ -68,23 +68,13 @@ public static function checkThisIdentity(TypeNode $returnTypeNode, mixed $value,
}
/**
- * Recursively resolves special type identifiers (self, static, parent, FQCNs, ConstFetch class names) in a TypeNode AST using Reflection context.
+ * Recursively resolves special type identifiers in a TypeNode AST using Reflection context.
*
* @param \ReflectionClass