Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 8 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,8 @@ permissions:

jobs:
test:
name: PHP ${{ matrix.php }} (${{ matrix.os }})
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
php: ['8.2', '8.3', '8.4', '8.5']
name: PHP 8.2 Debug (ubuntu-latest)
runs-on: ubuntu-latest

steps:
- name: Checkout Code
Expand All @@ -29,18 +23,17 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
php-version: '8.2'
extensions: dom, mbstring, zip, libxml, json, tokenizer, fileinfo
coverage: none

- name: Install Dependencies
uses: ramsey/composer-install@v3
with:
dependency-versions: ${{ matrix.php == '8.2' && 'lowest' || 'highest' }}
dependency-versions: lowest

- name: Run Static Analysis (PHPStan)
run: ./vendor/bin/phpstan analyse --no-progress
if: matrix.os == 'ubuntu-latest' && matrix.php == '8.3'
- name: Clear TypePHP Cache
run: php bin/typephp cache:clear

- name: Run Test Suite (Pest)
run: ./vendor/bin/pest --ci
- name: Run Test Suite (Pest) with Live Debug
run: ./vendor/bin/pest tests/TypeChecking/ArraysAndShapes/ClassConstKeyShapeTest.php --ci
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
<a href="https://phpstan.org/"><img src="https://img.shields.io/badge/PHPStan-Level%20MAX-brightgreen.svg?style=flat" alt="PHPStan Level MAX"></a>
</p>

------
---

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/)**
Expand All @@ -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

Expand Down
43 changes: 37 additions & 6 deletions src/Command/CommandRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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);
}
}
13 changes: 12 additions & 1 deletion src/Command/RunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
14 changes: 12 additions & 2 deletions src/Internal/ContractVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,9 +27,9 @@ public function __construct()
/**
* Traverses and transforms AST nodes during entry.
*
* @return array<Node>|null
* @return array<Node>|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
Expand All @@ -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;
Expand Down
14 changes: 7 additions & 7 deletions src/Internal/Visitor/PropertyHookInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand All @@ -96,4 +96,4 @@ public function enterNode(Node $n): int|null

return $newStmts;
}
}
}
Loading
Loading