Skip to content

Commit 456eaf8

Browse files
committed
Enhance ContractParser to include hasParamContract and hasReturnContract flags; update ParamChecker and ReturnChecker to utilize new contract properties
1 parent 520d85a commit 456eaf8

4 files changed

Lines changed: 67 additions & 14 deletions

File tree

src/Contract/ContractParser.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ final class ContractParser
3030
/**
3131
* Cache for resolved contract metadata.
3232
*
33-
* @var array<string, array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}>
33+
* @var array<string, array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>, hasParamContract: bool, hasReturnContract: bool}>
3434
*/
3535
private static array $cache = [];
3636

@@ -72,7 +72,7 @@ public static function reset(): void
7272
/**
7373
* Parses PHPDoc contracts for a function or class method.
7474
*
75-
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
75+
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>, hasParamContract: bool, hasReturnContract: bool}
7676
*/
7777
public static function parse(string $function): array
7878
{
@@ -100,17 +100,35 @@ public static function parse(string $function): array
100100
'classTemplates' => $classTemplates,
101101
'return' => null,
102102
'aliases' => $aliases,
103+
'hasParamContract' => false,
104+
'hasReturnContract' => false,
103105
];
104106
}
105107
} else {
106-
$contract = ['types' => [], 'templates' => [], 'classTemplates' => [], 'return' => null, 'aliases' => []];
108+
$contract = [
109+
'types' => [],
110+
'templates' => [],
111+
'classTemplates' => [],
112+
'return' => null,
113+
'aliases' => [],
114+
'hasParamContract' => false,
115+
'hasReturnContract' => false,
116+
];
107117
}
108118
} else {
109119
$ref = new \ReflectionFunction($function);
110120
$contract = self::parseFunction($ref);
111121
}
112122
} catch (\ReflectionException $e) {
113-
$contract = ['types' => [], 'templates' => [], 'classTemplates' => [], 'return' => null, 'aliases' => []];
123+
$contract = [
124+
'types' => [],
125+
'templates' => [],
126+
'classTemplates' => [],
127+
'return' => null,
128+
'aliases' => [],
129+
'hasParamContract' => false,
130+
'hasReturnContract' => false,
131+
];
114132
}
115133

116134
return self::$cache[$function] = $contract;
@@ -436,7 +454,7 @@ public static function parseClassAliases(string $className): array
436454
/**
437455
* Orchestrates parsing for class methods across the inheritance hierarchy.
438456
*
439-
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
457+
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>, hasParamContract: bool, hasReturnContract: bool}
440458
*/
441459
private static function parseMethod(\ReflectionMethod $ref): array
442460
{
@@ -459,13 +477,15 @@ private static function parseMethod(\ReflectionMethod $ref): array
459477
'classTemplates' => $classTemplates,
460478
'return' => $returnType,
461479
'aliases' => $aliases,
480+
'hasParamContract' => \count($types) > 0,
481+
'hasReturnContract' => $returnType !== null,
462482
];
463483
}
464484

465485
/**
466486
* Orchestrates parsing for standalone global or namespaced functions.
467487
*
468-
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>}
488+
* @return array{types: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>, classTemplates: array<string, TemplateTagValueNode>, return: ?TypeNode, aliases: array<string, TypeNode>, hasParamContract: bool, hasReturnContract: bool}
469489
*/
470490
private static function parseFunction(\ReflectionFunction $ref): array
471491
{
@@ -482,6 +502,8 @@ private static function parseFunction(\ReflectionFunction $ref): array
482502
'classTemplates' => [],
483503
'return' => null,
484504
'aliases' => [],
505+
'hasParamContract' => false,
506+
'hasReturnContract' => false,
485507
];
486508
}
487509

@@ -520,6 +542,8 @@ private static function parseFunction(\ReflectionFunction $ref): array
520542
'classTemplates' => [],
521543
'return' => $returnType,
522544
'aliases' => $aliases,
545+
'hasParamContract' => \count($types) > 0,
546+
'hasReturnContract' => $returnType !== null,
523547
];
524548
}
525549

@@ -846,4 +870,4 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo
846870

847871
return $node;
848872
}
849-
}
873+
}

src/Internal/Checker/ParamChecker.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ public static function checkParams(
6161
}
6262

6363
$contract = ContractParser::parse($effectiveFunction);
64-
if (\count($contract['types']) === 0) {
64+
65+
if (! $contract['hasParamContract']) {
6566
return null;
6667
}
6768

@@ -111,7 +112,7 @@ public static function checkParams(
111112
}
112113

113114
/**
114-
* Resolves the actual runtime class name vs trait name with $O(1)$ memoization.
115+
* Resolves the actual runtime class name vs trait name with O(1) memoization.
115116
*/
116117
private static function resolveEffectiveFunction(string $function, object|string|null $thisOrClass, ?object $thisObj): string
117118
{

src/Internal/Checker/ReturnChecker.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,12 @@ public static function checkReturn(
7171
}
7272

7373
$contract = ContractParser::parse($effectiveFunction);
74-
$returnTypeNode = $contract['return'] ?? null;
7574

75+
if (! ($contract['hasReturnContract'] ?? ($contract['return'] !== null))) {
76+
return $value;
77+
}
78+
79+
$returnTypeNode = $contract['return'];
7680
if ($returnTypeNode === null) {
7781
return $value;
7882
}
@@ -93,7 +97,7 @@ public static function checkReturn(
9397
}
9498

9599
/**
96-
* Resolves the actual runtime class name vs trait name with $O(1)$ memoization.
100+
* Resolves the actual runtime class name vs trait name with O(1) memoization.
97101
*/
98102
private static function resolveEffectiveFunction(string $function, object|string|null $thisOrClass, ?object $thisObj): string
99103
{
@@ -346,4 +350,4 @@ private static function resolveTemplateConditional(
346350

347351
return self::resolveConditionalReturnType($selectedBranch, $vars, $boundTemplates, $registry);
348352
}
349-
}
353+
}

src/Validator/ObjectShapeValidator.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,30 @@ public function validate(mixed $value, TypeNode $node, string $context, TypeVali
2323

2424
/** @var ObjectShapeNode $shapeNode */
2525
$shapeNode = $node;
26+
27+
if ($value instanceof \stdClass) {
28+
foreach ($shapeNode->items as $item) {
29+
$propName = (string) $item->keyName;
30+
31+
if (! property_exists($value, $propName) && ! isset($value->$propName)) {
32+
if (! $item->optional) {
33+
return ErrorFactory::createError($context . " is missing required property '$propName'");
34+
}
35+
36+
continue;
37+
}
38+
39+
$propValue = $value->$propName;
40+
41+
$err = $registry->validate($propValue, $item->valueType, '');
42+
if ($err !== null) {
43+
return ErrorFactory::createError($context . "->{$propName}" . $err->getMessage());
44+
}
45+
}
46+
47+
return null;
48+
}
49+
2650
$refObject = new \ReflectionObject($value);
2751

2852
foreach ($shapeNode->items as $item) {
@@ -53,9 +77,9 @@ public function validate(mixed $value, TypeNode $node, string $context, TypeVali
5377
$propValue = $value->$propName;
5478
}
5579

56-
$err = $registry->validate($propValue, $item->valueType, $context . "->{$propName}");
80+
$err = $registry->validate($propValue, $item->valueType, '');
5781
if ($err !== null) {
58-
return $err;
82+
return ErrorFactory::createError($context . "->{$propName}" . $err->getMessage());
5983
}
6084
}
6185

0 commit comments

Comments
 (0)