Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6116dcb
Add tests for dynamic class static property transformations and handl…
rcalicdan Sep 22, 2026
e8d3eb4
Refactor return node handling and add tests for variable name inferen…
rcalicdan Sep 22, 2026
b9a31ef
Add tests for doc comment and native union type extraction in Functio…
rcalicdan Sep 22, 2026
c621bcb
Refactor class instantiation syntax and improve test coverage for Pro…
rcalicdan Sep 22, 2026
76f72bb
Refactor IgnoreManager and enhance test coverage with new fixtures an…
rcalicdan Sep 22, 2026
f9ef669
Enhance test coverage for PathMatcher by adding tests for static sour…
rcalicdan Sep 22, 2026
ee973af
Enhance test coverage for Config by adding tests for project root res…
rcalicdan Sep 23, 2026
009910f
Refactor StubManager and enhance test coverage with new tests for res…
rcalicdan Sep 23, 2026
ef45579
Improve test coverage
rcalicdan Sep 23, 2026
fb9e596
Enhance test coverage for TypeFormatter by adding tests for various d…
rcalicdan Sep 23, 2026
b3bb9e8
Add new dedicated Validators Test suites tests and remove outdated Va…
rcalicdan Sep 24, 2026
3ea6a39
Add comprehensive tests for GenericValidator and ObjectShapeValidator
rcalicdan Sep 24, 2026
00343b7
fix code styling
rcalicdan Sep 24, 2026
c2fb988
make typep cli binary to type check any file without specifying in th…
rcalicdan Sep 24, 2026
5fd13e4
Add wildcard include pattern to configuration for broader file matching
rcalicdan Sep 24, 2026
0b73868
Refactor include handling in RunCommand to ensure only valid strings …
rcalicdan Sep 24, 2026
fb999d2
Update README.md to clarify CLI binary type-checking capabilities for…
rcalicdan Sep 24, 2026
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ composer.lock
index.php
benchmark.php
.php-cs-fixer.cache
/app
/app
/coverage-report
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
Here is the updated `README.md` reflecting that the CLI binary automatically type-checks any specified script without needing to register it in `typephp.php`:

```markdown
<h1 align="center">TypePHP</h1>

<p align="center">
Expand Down Expand Up @@ -76,23 +79,27 @@ Simply run your tests or use your local development server (**Laravel**, **Symfo

## Executing Standalone Scripts (CLI Runner)

For standalone, single-execution PHP scripts that do not have an explicit autoloader entry point, run them directly with the TypePHP CLI binary:
You can run any standalone PHP script directly with the TypePHP CLI binary:

```bash
vendor/bin/typephp script.php
```

Targeting a file directly with the CLI binary automatically type-checks that file—**even if it is not registered in `typephp.php` or falls outside your configured `include` paths**.

Any secondary files required or included by your script will continue to respect your project's configured `include` and `exclude` paths.

---

## Type-Checking Files Anywhere (`"*"` Wildcard Glob)

By default, TypePHP checks standard application folders (`src/**`, `app/**`, `tests/**`). To type-check PHP files anywhere in your project root while still respecting your excluded folders, set the `"*"` wildcard glob in `typephp.php`:
By default, TypePHP checks standard application folders (`src/**`, `app/**`, `tests/**`). To type-check PHP files anywhere in your project root during regular application runs while still respecting your excluded folders, set the `"*"` wildcard glob in `typephp.php`:

```php
// typephp.php
return [
'include' => [
'*', // Intercepts and type-checks PHP files anywhere in the project
'*',
],
'exclude' => [
'vendor/**',
Expand Down Expand Up @@ -167,14 +174,14 @@ Define generic templates and TypePHP tracks their state per object instance in m
class Collection
{
/** @param T $item */
public function add(mixed $item): void { /* ... */ }
public function add(mixed $item): void { }
}

// Prebind T = User to this specific instance in WeakMap memory
/** @var Collection<User> $users */
$users = new Collection();

$users->add(new User('Alice')); // Valid
$users->add(new User('Alice'));

$users->add(new Product('SKU-100'));
// Throws TypeError: Argument $item (template T = User) must be of type User, Product given
Expand Down Expand Up @@ -207,7 +214,7 @@ class DatabaseService

$service = new DatabaseService();

$service->connect(['driver' => 'pdo_mysql']); // Valid
$service->connect(['driver' => 'pdo_mysql']);

$service->connect(['driver' => 'pdo_invalid']);
// Throws TypeError: Argument $params['driver'] must be a key of DriverManager::DRIVER_MAP
Expand Down
8 changes: 7 additions & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
Expand All @@ -9,5 +12,8 @@
<include>
<directory>src</directory>
</include>
<exclude>
<file>src/bootstrap.php</file>
</exclude>
</source>
</phpunit>
2 changes: 1 addition & 1 deletion src/Internal/Ast/ContractVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public function enterNode(Node $node): ?array
return null;
}

if ($node instanceof Node\Stmt\Return_ && $node->expr !== null) {
if ($node instanceof Node\Stmt\Return_) {
$this->handleReturn($node);

return null;
Expand Down
19 changes: 3 additions & 16 deletions src/Internal/Ast/FunctionContractInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,23 +150,12 @@ private static function resolveDocComment(Node\Stmt\Function_|Node\Stmt\ClassMet
return $doc;
}

foreach ($node->getComments() as $comment) {
if ($comment instanceof Doc) {
return $comment;
}
}

if ($node->attrGroups !== []) {
foreach ($node->attrGroups as $group) {
$groupDoc = $group->getDocComment();
if ($groupDoc !== null) {
return $groupDoc;
}
foreach ($group->getComments() as $comment) {
if ($comment instanceof Doc) {
return $comment;
}
}
}
}

Expand Down Expand Up @@ -286,10 +275,6 @@ private static function resolveThisArg(bool $isClassMethod, Node\Stmt\Function_|

private static function isGenerator(Node\Stmt\Function_|Node\Stmt\ClassMethod $node): bool
{
if ($node->stmts === null) {
return false;
}

$visitor = new class () extends NodeVisitorAbstract {
public bool $isGen = false;

Expand All @@ -311,7 +296,9 @@ public function enterNode(Node $n): ?int

$traverser = new NodeTraverser();
$traverser->addVisitor($visitor);
$traverser->traverse($node->stmts);
/** @var array<Node\Stmt> $stmts */
$stmts = $node->stmts;
$traverser->traverse($stmts);

return $visitor->isGen;
}
Expand Down
24 changes: 23 additions & 1 deletion src/Internal/Cli/RunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace TypePHP\Internal\Cli;

use TypePHP\Internal\Util\Config;
use TypePHP\TypePHP;

/**
Expand Down Expand Up @@ -54,8 +55,29 @@ public function execute(array $args, $outputStream = STDOUT, $errorStream = STDE
}

try {
TypePHP::boot();
$realTarget = realpath($target);
if ($realTarget !== false) {
$normalizedTarget = str_replace('\\', '/', $realTarget);
$currentConfig = Config::get();
$includes = [];

if (\is_array($currentConfig['include'] ?? null)) {
foreach ($currentConfig['include'] as $inc) {
if (\is_string($inc)) {
$includes[] = $inc;
}
}
}

$includes[] = $normalizedTarget;

Config::set([
'include' => array_values(array_unique($includes)),
]);
}

TypePHP::boot();

if ($realTarget !== false) {
require $realTarget;
}
Expand Down
33 changes: 14 additions & 19 deletions src/Internal/Diagnostic/ErrorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace TypePHP\Internal\Diagnostic;

use ReflectionClass;
use Throwable;
use TypeError;

/**
Expand Down Expand Up @@ -168,28 +167,24 @@ private static function mutateException(
?int $targetLine,
array $filteredTrace
): void {
try {
$ref = new ReflectionClass(\Error::class);
$ref = new ReflectionClass(\Error::class);

$propMessage = $ref->getProperty('message');
$propMessage->setValue($e, $message);
$propMessage = $ref->getProperty('message');
$propMessage->setValue($e, $message);

if ($targetFile !== null) {
$propFile = $ref->getProperty('file');
$propFile->setValue($e, $targetFile);
}
if ($targetFile !== null) {
$propFile = $ref->getProperty('file');
$propFile->setValue($e, $targetFile);
}

if ($targetLine !== null) {
$propLine = $ref->getProperty('line');
$propLine->setValue($e, $targetLine);
}
if ($targetLine !== null) {
$propLine = $ref->getProperty('line');
$propLine->setValue($e, $targetLine);
}

if (\count($filteredTrace) > 0) {
$propTrace = $ref->getProperty('trace');
$propTrace->setValue($e, $filteredTrace);
}
} catch (Throwable $err) {
// Silently fallback if reflection mutation fails
if (\count($filteredTrace) > 0) {
$propTrace = $ref->getProperty('trace');
$propTrace->setValue($e, $filteredTrace);
}
}
}
6 changes: 1 addition & 5 deletions src/Internal/Diagnostic/TypeFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,7 @@ public static function formatGivenValue(mixed $value, bool $isSensitive = false)
return "associative array (key '$firstStringKey')";
}

if ($firstNonSequentialIndex !== null) {
return "non-sequential array (index $firstNonSequentialIndex)";
}

return 'array (' . \count($value) . ' items)';
return "non-sequential array (index $firstNonSequentialIndex)";
}

if (\is_bool($value)) {
Expand Down
8 changes: 4 additions & 4 deletions src/Internal/Util/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -284,14 +284,14 @@ public static function getArrayValidationStrategy(): string
* Locates the project root directory by searching upwards for vendor/autoload.php or composer.json.
* Caches the result in memory so the search happens exactly once.
*/
public static function getProjectRoot(): string
public static function getProjectRoot(?string $startingDir = null): string
{
if (self::$projectRoot !== null) {
if ($startingDir === null && self::$projectRoot !== null) {
return self::$projectRoot;
}

$cwd = getcwd();
if ($cwd !== false) {
if ($startingDir === null && $cwd !== false) {
$realCwd = realpath($cwd) !== false ? realpath($cwd) : $cwd;
$normCwd = rtrim(str_replace('\\', '/', (string) $realCwd), '/');
if (
Expand All @@ -302,7 +302,7 @@ public static function getProjectRoot(): string
}
}

$dir = str_replace('\\', '/', __DIR__);
$dir = str_replace('\\', '/', $startingDir ?? __DIR__);

if (str_contains($dir, '/vendor/')) {
$vendorPos = strrpos($dir, '/vendor/');
Expand Down
34 changes: 9 additions & 25 deletions src/Internal/Util/IgnoreManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use ReflectionClass;
use ReflectionFunction;
use Throwable;

/**
* @internal High-performance manager for resolving file-level and caller-level @typephp-ignore tags.
Expand Down Expand Up @@ -112,7 +111,7 @@ public static function isCallerIgnored(?string $callerClass = null, ?string $cal
continue;
}

if (str_starts_with($function, '__pest_')) {
if (str_contains($function, '__pest_')) {
continue;
}

Expand Down Expand Up @@ -198,25 +197,17 @@ private static function checkMethodIgnored(string $class, string $method): bool
return false;
}

try {
/** @var class-string<object> $class */
$refClass = new ReflectionClass($class);
/** @var class-string<object> $class */
$refClass = new ReflectionClass($class);

if ($refClass->hasMethod($method)) {
$refMethod = $refClass->getMethod($method);
if (self::hasIgnoreDocTag($refMethod->getDocComment())) {
return true;
}
}

if (self::hasIgnoreDocTag($refClass->getDocComment())) {
if ($refClass->hasMethod($method)) {
$refMethod = $refClass->getMethod($method);
if (self::hasIgnoreDocTag($refMethod->getDocComment())) {
return true;
}
} catch (Throwable $e) {
return false;
}

return false;
return self::hasIgnoreDocTag($refClass->getDocComment());
}

private static function checkFunctionIgnored(string $function): bool
Expand All @@ -232,15 +223,8 @@ private static function checkFunctionIgnored(string $function): bool
return false;
}

try {
$refFunc = new ReflectionFunction($function);
if (self::hasIgnoreDocTag($refFunc->getDocComment())) {
return true;
}
} catch (Throwable $e) {
return false;
}
$refFunc = new ReflectionFunction($function);

return false;
return self::hasIgnoreDocTag($refFunc->getDocComment());
}
}
4 changes: 0 additions & 4 deletions src/Internal/Util/PathMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,6 @@ public static function isStaticSourcePath(string $normalizedPath): bool
return true;
}

if (preg_match('#(^|/)packages/[^/]+/src/#', $canon) === 1) {
return true;
}

return false;
}

Expand Down
Loading
Loading