Skip to content

Commit 746d352

Browse files
authored
Quick patch 3 (#16)
* Enhance ContractVisitor to respect @typephp-ignore tags and update tests for inline variable validation * Fix formatting and improve clarity in README.md * Enhance CommandRunner to handle unknown commands and validate PHP file extensions; add corresponding tests * Reorganize test suites for typechecking * Remove reorganize-tests.sh script for test directory restructuring * Add class constant key tests and fixtures for array shape validation * Add ClassStringFactoryContainer and corresponding tests for class-string<Countable> validation * Refactor SpecialTypeResolver to extract class and constant names for improved key resolution in reflection contexts to support 8.2 below * Improve property hook checks and enhance node handling in PropertyHookInjector * Refactor CI workflow for PHP 8.2 and streamline test execution; add debug logging in SpecialTypeResolver * Refactor code style for consistency in SpecialTypeResolver; update cache clear command in CI workflow * Fix typos and improve debug logging in SpecialTypeResolver; correct GenericTypeNode instantiation and add detailed logging for array shape resolution * Fix typos and improve type handling in SpecialTypeResolver; correct GenericTypeNode instantiation and enhance debug logging * Enhance debug logging in SpecialTypeResolver for PHP 8.2 compatibility; add runtime exception for specific AST node values * Enhance debug logging in SpecialTypeResolver; add detailed output for key type resolution and constant reflection * Add support for wrapping class constant array shape keys in quotes for legacy phpdoc-parser compatibility in DocblockNormalizer * Refactor CI workflow to use matrix strategy for PHP versions and OS; update dependency installation logic and streamline test execution
1 parent 9ce80d2 commit 746d352

56 files changed

Lines changed: 704 additions & 347 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
<a href="https://phpstan.org/"><img src="https://img.shields.io/badge/PHPStan-Level%20MAX-brightgreen.svg?style=flat" alt="PHPStan Level MAX"></a>
1515
</p>
1616

17-
------
17+
---
1818

19-
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.
19+
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.
2020

2121

2222
**[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
2929

3030
* [Getting Started & Installation Guide](https://typephp-php.github.io/typephp/getting-started/installation)
3131
* [Quick Start Guide](https://typephp-php.github.io/typephp/getting-started/quick-start)
32-
* [Architecture: How It Works](https://typephp-php.github.io/typephp/architecture/how-it-works)
33-
* [Core Concepts: Function Contracts](https://typephp-php.github.io/typephp/core-concepts/function-contracts)
34-
* [Core Concepts: Generics & Bounds](https://typephp-php.github.io/typephp/core-concepts/generics-and-bounds)
32+
* [Configuration Guide](https://typephp-php.github.io/typephp/getting-started/configuration)
33+
* [CLI Commands Reference](https://typephp-php.github.io/typephp/getting-started/cli-commands)
34+
* [Enforcement Boundaries: Function Contracts](https://typephp-php.github.io/typephp/core-concepts/function-contracts)
35+
* [Runtime Generics & Bounds](https://typephp-php.github.io/typephp/generics/generics-and-bounds)
3536
* [Supported Types: Arrays & Shapes](https://typephp-php.github.io/typephp/supported-types/arrays-and-shapes)
36-
* [Troubleshooting & FAQ](https://typephp-php.github.io/typephp/advanced/troubleshooting)
37+
* [Architecture: How It Works](https://typephp-php.github.io/typephp/advanced/how-it-works)
38+
* [Troubleshooting & FAQ](https://typephp-php.github.io/typephp/troubleshooting)
3739

3840
## Inspiration
3941

40-
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.
42+
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.
4143

4244
## Sponsors
4345

src/Command/CommandRunner.php

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66

77
final class CommandRunner
88
{
9+
private const KNOWN_COMMANDS = [
10+
'config:init',
11+
'cache:clear',
12+
'cache:warm',
13+
'cache:rebuild',
14+
'help',
15+
];
16+
917
/**
1018
* Parses CLI arguments and routes execution to the corresponding command class.
1119
*
@@ -15,28 +23,51 @@ final class CommandRunner
1523
*/
1624
public static function run(array $args, $outputStream = STDOUT, $errorStream = STDERR): int
1725
{
18-
$showHelp = \in_array('help', $args, true) || \in_array('typephp:help', $args, true) || \in_array('--help', $args, true) || \in_array('-h', $args, true);
26+
$c = [CliFormatter::class, 'color'];
27+
28+
$showHelp = \in_array('help', $args, true)
29+
|| \in_array('typephp:help', $args, true)
30+
|| \in_array('--help', $args, true)
31+
|| \in_array('-h', $args, true)
32+
|| $args === [];
1933

20-
if ($showHelp || $args === []) {
34+
if ($showHelp) {
2135
return (new HelpCommand())->execute($args, $outputStream, $errorStream);
2236
}
2337

24-
if (\in_array('config:init', $args, true) || \in_array('init', $args, true)) {
38+
$firstArg = $args[0] ?? '';
39+
40+
if ($firstArg === 'config:init' || $firstArg === 'init') {
2541
return (new ConfigInitCommand())->execute($args, $outputStream, $errorStream);
2642
}
2743

28-
if (\in_array('cache:rebuild', $args, true)) {
44+
if ($firstArg === 'cache:rebuild') {
2945
return (new CacheRebuildCommand())->execute($args, $outputStream, $errorStream);
3046
}
3147

32-
if (\in_array('cache:clear', $args, true)) {
48+
if ($firstArg === 'cache:clear') {
3349
return (new CacheClearCommand())->execute($args, $outputStream, $errorStream);
3450
}
3551

36-
if (\in_array('cache:warm', $args, true)) {
52+
if ($firstArg === 'cache:warm') {
3753
return (new CacheWarmCommand())->execute($args, $outputStream, $errorStream);
3854
}
3955

56+
$hasFileExtension = str_contains(basename($firstArg), '.');
57+
$isFileTarget = file_exists($firstArg) || $hasFileExtension;
58+
59+
if (! $isFileTarget) {
60+
fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n");
61+
fwrite($errorStream, ' ' . $c('', 'red') . ' Command ' . $c('"' . $firstArg . '"', 'bold') . " is not defined.\n\n");
62+
fwrite($errorStream, ' ' . $c('Did you mean one of these?', 'yellow') . "\n");
63+
foreach (self::KNOWN_COMMANDS as $cmd) {
64+
fwrite($errorStream, ' ' . $c('', 'cyan') . ' ' . $cmd . "\n");
65+
}
66+
fwrite($errorStream, "\n");
67+
68+
return 1;
69+
}
70+
4071
return (new RunCommand())->execute($args, $outputStream, $errorStream);
4172
}
4273
}

src/Command/RunCommand.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
*/
1212
final class RunCommand implements CommandInterface
1313
{
14+
private const VALID_PHP_EXTENSIONS = ['php', 'phtml', 'php5', 'php7', 'php8', 'phps'];
15+
1416
public function execute(array $args, $outputStream = STDOUT, $errorStream = STDERR): int
1517
{
1618
$c = [CliFormatter::class, 'color'];
@@ -21,6 +23,15 @@ public function execute(array $args, $outputStream = STDOUT, $errorStream = STDE
2123
foreach ($args as $arg) {
2224
if (! str_starts_with($arg, '--') && ! str_starts_with($arg, '-')) {
2325
$givenTargetCandidate = $arg;
26+
$ext = strtolower(pathinfo($arg, PATHINFO_EXTENSION));
27+
28+
if ($ext !== '' && ! \in_array($ext, self::VALID_PHP_EXTENSIONS, true)) {
29+
fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n");
30+
fwrite($errorStream, ' ' . $c('', 'red') . ' Target file ' . $c('"' . $arg . '"', 'bold') . " is not a PHP script file. TypePHP can only execute PHP files.\n\n");
31+
32+
return 1;
33+
}
34+
2435
if (file_exists($arg)) {
2536
$target = $arg;
2637
}
@@ -31,7 +42,7 @@ public function execute(array $args, $outputStream = STDOUT, $errorStream = STDE
3142

3243
if ($givenTargetCandidate !== null && $target === null) {
3344
fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n");
34-
fwrite($errorStream, ' ' . $c('', 'red') . ' Target file ' . $c('"' . $givenTargetCandidate . '"', 'bold') . " does not exist or is not readable.\n\n");
45+
fwrite($errorStream, ' ' . $c('', 'red') . ' Target script file ' . $c('"' . $givenTargetCandidate . '"', 'bold') . " does not exist or is not readable.\n\n");
3546

3647
return 1;
3748
}

src/Internal/ContractVisitor.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace TypePHP\Internal;
66

77
use PhpParser\Node;
8+
use PhpParser\NodeTraverser;
89
use PhpParser\NodeVisitorAbstract;
910
use TypePHP\Internal\Visitor\FunctionContractInjector;
1011
use TypePHP\Internal\Visitor\NodeBuilder;
@@ -26,9 +27,9 @@ public function __construct()
2627
/**
2728
* Traverses and transforms AST nodes during entry.
2829
*
29-
* @return array<Node>|null
30+
* @return array<Node>|int|null
3031
*/
31-
public function enterNode(Node $node): array|null
32+
public function enterNode(Node $node): array|int|null
3233
{
3334
if ($node instanceof Node\Stmt\Function_
3435
|| $node instanceof Node\Stmt\ClassMethod
@@ -47,6 +48,15 @@ public function enterNode(Node $node): array|null
4748
}
4849

4950
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
51+
$doc = $node->getDocComment();
52+
if ($doc !== null) {
53+
$docText = $doc->getText();
54+
$shouldRespectIgnore = (bool) (Config::get()['respect_ignore_tags'] ?? true);
55+
if ($shouldRespectIgnore && (str_contains($docText, '@typephp-ignore') || str_contains($docText, '@typephp-disable'))) {
56+
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
57+
}
58+
}
59+
5060
FunctionContractInjector::inject($node);
5161

5262
return null;

src/Internal/DocblockNormalizer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ final class DocblockNormalizer
2828
*/
2929
public static function normalize(string $doc): string
3030
{
31+
$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;
32+
3133
if (! str_contains($doc, '{')) {
3234
return $doc;
3335
}

src/Internal/Visitor/PropertyHookInjector.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ final class PropertyHookInjector
1616
{
1717
public static function process(Node\Stmt\Property $node): void
1818
{
19-
if ($node->hooks === []) {
19+
if (! isset($node->hooks) || ! \is_array($node->hooks) || $node->hooks === []) {
2020
return;
2121
}
2222

@@ -76,15 +76,15 @@ public function __construct(private string $propertyName)
7676
{
7777
}
7878

79-
public function enterNode(Node $n): int|null
79+
public function enterNode(Node $node): int|null
8080
{
81-
if ($n instanceof Node\Expr\Closure || $n instanceof Node\Expr\ArrowFunction || $n instanceof Node\Stmt\Function_ || $n instanceof Node\Stmt\ClassMethod) {
81+
if ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction || $node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
8282
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
8383
}
8484

85-
if ($n instanceof Node\Stmt\Return_ && $n->expr !== null) {
86-
$checkCall = NodeBuilder::createPropertyCheckCall($n->expr, new Node\Expr\Variable('this'), $this->propertyName);
87-
$n->expr = NodeBuilder::createTernaryThrowExpr($checkCall);
85+
if ($node instanceof Node\Stmt\Return_ && $node->expr !== null) {
86+
$checkCall = NodeBuilder::createPropertyCheckCall($node->expr, new Node\Expr\Variable('this'), $this->propertyName);
87+
$node->expr = NodeBuilder::createTernaryThrowExpr($checkCall);
8888
}
8989

9090
return null;

0 commit comments

Comments
 (0)