Skip to content

Commit 4e4244e

Browse files
authored
Enhance Docblock Normalization for nested Callable docblock syntax and Improve .env Framework Config handling (#107)
* Enhance Docblock Normalization for nested Callable docblock syntax and Config Handling via env for imrpove framework dotenv integration * Refactor environment variable handling in tests for improved testing isolation * Refactor getEnvValue method for improved null handling and type safety; update tests for consistency
1 parent 08fa491 commit 4e4244e

6 files changed

Lines changed: 538 additions & 353 deletions

File tree

‎src/Internal/Docblock/DocblockNormalizer.php‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* Converts class-specific shapes like stdClass{id: int} into intersection shapes (stdClass & object{id: int}).
1010
* Normalizes variadic tuple spread syntax (...Type[] and ...list<Type>) into PHPStan-compliant ...<Type> syntax.
1111
* Normalizes unqualified wildcard bitmasks (int-mask-of<E_*>) into quoted string literals.
12+
* Normalizes unparenthesized nested callable return types into right-associative parenthesized callables.
1213
*
1314
* @internal
1415
*/
@@ -52,7 +53,13 @@ public static function normalize(string $doc): string
5253
}
5354

5455
if (stripos($doc, 'callable') !== false || stripos($doc, 'closure') !== false) {
56+
// 1. Auto-complete omitted return types to ': mixed'
5557
$doc = preg_replace('/(callable|Closure|static-closure)\s*(\((?:[^()]|\((?:[^()]|\([^()]*\))*\))*\))(?!\s*:)/i', '$1$2: mixed', $doc) ?? $doc;
58+
59+
// 2. Auto-parenthesize unparenthesized nested return callables (Mago / Right-associative functional currying)
60+
if (str_contains($doc, ':')) {
61+
$doc = self::normalizeNestedCallables($doc);
62+
}
5663
}
5764

5865
if (str_contains($doc, '::') && str_contains($doc, ':')) {
@@ -75,6 +82,108 @@ public static function normalize(string $doc): string
7582
return self::normalizeShapes($doc);
7683
}
7784

85+
/**
86+
* Normalizes unparenthesized nested callable return types into right-associative parenthesized callables:
87+
* callable(int): callable(string): bool ==> callable(int): (callable(string): bool)
88+
*/
89+
private static function normalizeNestedCallables(string $doc): string
90+
{
91+
$len = \strlen($doc);
92+
$result = '';
93+
$i = 0;
94+
95+
while ($i < $len) {
96+
if ($doc[$i] === ':') {
97+
$j = $i + 1;
98+
while ($j < $len && ($doc[$j] === ' ' || $doc[$j] === "\t")) {
99+
$j++;
100+
}
101+
102+
if ($j < $len && $doc[$j] === '(') {
103+
$result .= $doc[$i];
104+
$i++;
105+
106+
continue;
107+
}
108+
109+
$remaining = substr($doc, $j);
110+
if (preg_match('/^(callable|Closure|static-closure)\s*\(/i', $remaining) === 1) {
111+
$result .= ': (';
112+
113+
$k = $j;
114+
$parenDepth = 0;
115+
$braceDepth = 0;
116+
$angleDepth = 0;
117+
$bracketDepth = 0;
118+
119+
while ($k < $len) {
120+
$char = $doc[$k];
121+
122+
if ($char === '(') {
123+
$parenDepth++;
124+
} elseif ($char === ')') {
125+
if ($parenDepth === 0) {
126+
break;
127+
}
128+
$parenDepth--;
129+
} elseif ($char === '{') {
130+
$braceDepth++;
131+
} elseif ($char === '}') {
132+
if ($braceDepth === 0) {
133+
break;
134+
}
135+
$braceDepth--;
136+
} elseif ($char === '<') {
137+
$angleDepth++;
138+
} elseif ($char === '>') {
139+
if ($angleDepth === 0) {
140+
break;
141+
}
142+
$angleDepth--;
143+
} elseif ($char === '[') {
144+
$bracketDepth++;
145+
} elseif ($char === ']') {
146+
if ($bracketDepth === 0) {
147+
break;
148+
}
149+
$bracketDepth--;
150+
} elseif ($char === ',' && $parenDepth === 0 && $braceDepth === 0 && $angleDepth === 0 && $bracketDepth === 0) {
151+
break;
152+
} elseif (($char === ' ' || $char === "\t" || $char === "\r" || $char === "\n") && $parenDepth === 0 && $braceDepth === 0 && $angleDepth === 0 && $bracketDepth === 0) {
153+
$afterWs = substr($doc, $k);
154+
155+
if (preg_match('/^\s+(\$[a-zA-Z_\x80-\xff]|\*\/|\*|\r|\n)/', $afterWs) === 1) {
156+
break;
157+
}
158+
159+
if (preg_match('/^\s+([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*\(/', $afterWs, $nameMatch) === 1) {
160+
$word = strtolower($nameMatch[1]);
161+
if (! \in_array($word, ['callable', 'closure', 'static-closure', 'array', 'list', 'object', 'iterable'], true)) {
162+
break;
163+
}
164+
}
165+
}
166+
167+
$k++;
168+
}
169+
170+
$nestedCallableSubstring = substr($doc, $j, $k - $j);
171+
$normalizedNested = self::normalizeNestedCallables($nestedCallableSubstring);
172+
173+
$result .= $normalizedNested . ')';
174+
$i = $k;
175+
176+
continue;
177+
}
178+
}
179+
180+
$result .= $doc[$i];
181+
$i++;
182+
}
183+
184+
return $result;
185+
}
186+
78187
/**
79188
* Normalizes custom class shapes supporting arbitrary nested balanced braces via PCRE recursion.
80189
*/

‎src/Internal/Util/Config.php‎

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,52 @@ final class Config
9393

9494
private static int $ignoreTraceDepth = 25;
9595

96+
private static function getEnvValue(string $key): ?string
97+
{
98+
if (isset($_ENV[$key])) {
99+
$val = $_ENV[$key];
100+
} elseif (isset($_SERVER[$key])) {
101+
$val = $_SERVER[$key];
102+
} else {
103+
$env = getenv($key);
104+
$val = $env !== false ? $env : null;
105+
}
106+
107+
if ($val === null) {
108+
return null;
109+
}
110+
111+
if (\is_string($val)) {
112+
return $val;
113+
}
114+
115+
if (\is_scalar($val)) {
116+
return (string) $val;
117+
}
118+
119+
return null;
120+
}
121+
96122
public static function isEnabled(): bool
97123
{
124+
$envDisable = self::getEnvValue('TYPEPHP_DISABLE');
125+
if ($envDisable !== null && trim($envDisable) !== '') {
126+
if (filter_var($envDisable, FILTER_VALIDATE_BOOLEAN)) {
127+
return false;
128+
}
129+
}
130+
131+
$envEnabled = self::getEnvValue('TYPEPHP_ENABLED');
132+
if ($envEnabled !== null && trim($envEnabled) !== '') {
133+
if (! filter_var($envEnabled, FILTER_VALIDATE_BOOLEAN)) {
134+
return false;
135+
}
136+
}
137+
138+
if (\defined('TYPEPHP_DISABLE') && TYPEPHP_DISABLE) {
139+
return false;
140+
}
141+
98142
if (self::$cachedConfig === null) {
99143
self::get();
100144
}
@@ -104,8 +148,8 @@ public static function isEnabled(): bool
104148

105149
public static function isAutoBootEnabled(): bool
106150
{
107-
$envAutoBoot = getenv('TYPEPHP_AUTO_BOOT');
108-
if ($envAutoBoot !== false && trim((string) $envAutoBoot) !== '') {
151+
$envAutoBoot = self::getEnvValue('TYPEPHP_AUTO_BOOT');
152+
if ($envAutoBoot !== null && trim($envAutoBoot) !== '') {
109153
return filter_var($envAutoBoot, FILTER_VALIDATE_BOOLEAN);
110154
}
111155

@@ -118,9 +162,9 @@ public static function isAutoBootEnabled(): bool
118162

119163
public static function getOnViolation(): string
120164
{
121-
$envMode = getenv('TYPEPHP_ON_VIOLATION');
122-
if ($envMode !== false && trim((string) $envMode) !== '') {
123-
$normalized = strtolower(trim((string) $envMode));
165+
$envMode = self::getEnvValue('TYPEPHP_ON_VIOLATION');
166+
if ($envMode !== null && trim($envMode) !== '') {
167+
$normalized = strtolower(trim($envMode));
124168
if ($normalized === 'warning') {
125169
return 'warn';
126170
}
@@ -137,9 +181,9 @@ public static function getOnViolation(): string
137181

138182
public static function getReportFile(): ?string
139183
{
140-
$envFile = getenv('TYPEPHP_REPORT_FILE');
141-
if ($envFile !== false && trim((string) $envFile) !== '') {
142-
return trim((string) $envFile);
184+
$envFile = self::getEnvValue('TYPEPHP_REPORT_FILE');
185+
if ($envFile !== null && trim($envFile) !== '') {
186+
return trim($envFile);
143187
}
144188

145189
if (self::$cachedConfig === null) {
@@ -151,8 +195,8 @@ public static function getReportFile(): ?string
151195

152196
public static function isFailOnReportEnabled(): bool
153197
{
154-
$envFail = getenv('TYPEPHP_FAIL_ON_REPORT');
155-
if ($envFail !== false && trim((string) $envFail) !== '') {
198+
$envFail = self::getEnvValue('TYPEPHP_FAIL_ON_REPORT');
199+
if ($envFail !== null && trim($envFail) !== '') {
156200
return filter_var($envFail, FILTER_VALIDATE_BOOLEAN);
157201
}
158202

@@ -165,8 +209,8 @@ public static function isFailOnReportEnabled(): bool
165209

166210
public static function isRedactValuesEnabled(): bool
167211
{
168-
$envRedact = getenv('TYPEPHP_REDACT_VALUES');
169-
if ($envRedact !== false && trim((string) $envRedact) !== '') {
212+
$envRedact = self::getEnvValue('TYPEPHP_REDACT_VALUES');
213+
if ($envRedact !== null && trim($envRedact) !== '') {
170214
return filter_var($envRedact, FILTER_VALIDATE_BOOLEAN);
171215
}
172216

‎src/bootstrap.php‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
if (class_exists(TypePHP::class) && ! \defined('TYPEPHP_BOOTED')) {
1010
\define('TYPEPHP_BOOTED', true);
1111

12-
$isDisabledEnv = getenv('TYPEPHP_DISABLE') !== false && filter_var(getenv('TYPEPHP_DISABLE'), FILTER_VALIDATE_BOOLEAN);
12+
$isDisabledEnv = (
13+
(getenv('TYPEPHP_DISABLE') !== false && filter_var(getenv('TYPEPHP_DISABLE'), FILTER_VALIDATE_BOOLEAN))
14+
|| (isset($_ENV['TYPEPHP_DISABLE']) && filter_var($_ENV['TYPEPHP_DISABLE'], FILTER_VALIDATE_BOOLEAN))
15+
|| (isset($_SERVER['TYPEPHP_DISABLE']) && filter_var($_SERVER['TYPEPHP_DISABLE'], FILTER_VALIDATE_BOOLEAN))
16+
);
1317
$isDisabledConst = \defined('TYPEPHP_DISABLE') && TYPEPHP_DISABLE;
1418

1519
$isTooling = false;

0 commit comments

Comments
 (0)