ParseUrlFailureDereferenced
warning native on by defaultGroup: Probable bugs
Identify ordinary unguarded reads of dynamic parse_url results, or literal results with a supported parsing-failure proof. Restrict to array-returning mode; missing keys are another concern.
Detection
A resolved same-file reference parameter, reference capture or global writer can replace a checked result. Retain the producer of that replacement while clearing the earlier binding proof. Giving an element to an unresolved method does not rebind the containing array.
Follow each producer reaching a local read, including results written on an earlier loop turn or before a backward jump. A check of an earlier result does not check a later replacement; a check repeated before every consumption does. Ignore producers belonging to other result contracts when selecting an unchecked result.
- Report ordinary unguarded reads of dynamic parse_url results, or literal results with a supported parsing-failure proof. Empty HTTP(S) authorities and ports above 65535 establish such a proof. Restrict to array-returning mode; missing keys are another concern.
- Resolve builtin functions, classes, constants and methods, including imports and supported named arguments. Follow only bounded local facts within one lexical scope. Receiver and argument provenance must establish the specific API contract; variable names and comments never prove intent.
- Report one finding per violating operation. A reachable path, unchanged value identity and applicable API behavior are required. Intervening mutation, unknown calls involving tracked values, escaped aliases or exhausted analysis budgets invalidate proof.
- When several assignments reach the read (a loop that reassigns the variable, or an assignment made on one path only), the read is reported if one of them is a result of this API that no guard on its own path covers. A guard written before the other assignment does not cover it; a test inside the loop, or after the conditional assignment, covers every assignment. A reaching value that is not a result of this API (a literal, another call, an unknown value) never causes a report by itself. It does not hide one either: an unguarded result of another call reaching the same read leaves a result of this API reported.
- A result assigned inside a condition (
if (($parts = parse_url($raw)) === false) { … }) is tracked like one assigned in its own statement. Only the conditions of D4 guard it: an operator whose outcome does not fix the comparison (xor,^, a comparison between two conditions) proves nothing, and neither does a branch that does not exit.|and&between two tests evaluate both: the comparison is fixed where|yields false and where&yields true, and nowhere else. - D4. A read is guarded when a condition on the same variable, written after the producing call and before any reassignment, excludes
falseon every path to the read. Accepted condition shapes:- strict or loose comparison with
false($parts === false,false != $parts); - plain truthiness (
$parts,!$parts) andempty($parts); is_array($parts)andarray_key_exists($key, $parts)resolved to the builtins;issetoremptyon a key of the variable (isset($parts['host']),!empty($parts['path']));isset($parts)alone proves nothing becausefalseis set.- negation, and
&&/||(alsoand/or) combinations: a true conjunction needs one proving operand, a false conjunction needs all of them, and symmetrically for disjunctions.
- strict or loose comparison with
- D5. A guard applies in these positions: the matching branch of an enclosing
if/elseif/else(later branches also know the earlier conditions failed); the matching arm of an enclosing ternary; the right operand of&&or||whose left operand is the guard; and any statement after an earlier siblingifwhose main body always exits, at every enclosing block level inside the same function scope.
Only completely followed same-file bodies export replacement producers. An unstarted generator closure does not establish that its body wrote a new result, including when its syntactic yield lies in a skipped constant branch or ternary arm; advancing generators is outside this local producer contract.
Exceptions (no report)
Array and list destructuring destinations create slots rather than reading them. Destructuring keys and ordinary array values still require reads.
Literal URL shapes outside the supported failure proof receive no finding. This avoids requesting runtime failure guards for constants whose parsing outcome is unsupported.
Guarded probes with isset, empty or null coalescing, unsets and plain nested slot creation do not read a failed or missing value. Index expressions and the fallback operand of null coalescing still require normal reads.
Reads guarded as described by D4 and D5 never index the failure value.
A value the shared flow analysis proves is not false at the read is never reported, whatever form the proof takes: the conditions of D4 in the positions of D5, a loop condition, a statement that leaves on the failing outcome (
$parts !== false or exit;,$parts or throw …;,$parts ?: throw …;), a failing branch that ends in a call that never returns (a callee declarednever, or a function or a final or private method every path of which throws or exits), a test stored in a boolean and tested later while neither the boolean nor the tested variable has been written, and, on PHP 8.0 and later outside anytrywith acatch, a builtin call that returned after receiving the value for a parameter that only accepts arrays, objects or callables (count($parts)throws aTypeErrorforfalse; before PHP 8 it warns and returns, which proves nothing). Aswitch (true)case is read like the condition of anif/elseifchain; in aswitchon the variable itself with constant labels, the cases after afalseornulllabel and thedefaulthold a value that is neither, as does atruecase. Positions PHP may skip prove nothing: the right side of??=, the arguments of a null-safe call and anything chained after?->. A null-safe call to a never-returning method does not end the path, andf(...)creates a closure without callingf. An arm ofmatch (true)is read like aswitch (true)case; an arm that throws, exits or calls a function that never returns takes no part in what holds after thematch.|and&between two tests are read like||and&&for the outcome that fixes both operands (a false|, a true&), and a strictin_array($v, [...], true)over literals that include neitherfalsenornullexcludes both where it holds.gotojumps are followed, including a jump out of atryorcatchblock, which runs thefinallyblocks it leaves on its way. Where agotolabel is written before the use, and after the place where its scope first names the checked variable, a variable that can hold the same value, or code that may write any variable (extract,compact,get_defined_vars,parse_strgiven one argument, a variable variable,$GLOBALS,eval, an included file), a jump can land between a guard and the use: only what the flow analysis establishes counts there, and a guard recognized by where it is written proves nothing. Two variables can hold the same value when one is written from the other by a plain assignment, an assignment by reference or??=, when aforeachtakes one out of the other, or through a chain of those; in the value assigned, the receiver and the arguments of a call, the operands of an operator other than??, the variables a string embeds and an element key do not count, and a statement that only reads (echo $prefix . $parts['host'];) ties nothing. A parameter is named from the start of the function, and a checked expression that reads$thisor no variable counts every label before it. In a scope the flow analysis does not follow (one that evaluates or includes code, which may write any variable) a guarded use after such a label is therefore reported. A generator is followed: while it is suspended at ayield, the code that consumes it may write only what an unknown call may write, and, when the generator returns by reference, the variable it yields. A label written before all of that, such as aretry:label ahead of thetrythat produces and checks the value, changes nothing, whatever else the function reads beside the checked value; at file level every label written before the use counts. What was established of astaticlocal holds until the next call, or code that runs without a call being written (an object converted to text, a magic property or offset access, the destructor of an object a variable held when it is given another value), that may enter the function again and assign the variable. A call of PHP's own counts as such when it may run code of the project: it is given a callback other than the name of a PHP function that only computes on text and numbers and raises no warning whatever text it is given (a closed list:strlen,trim,intvaland the like, nothexdec,absorchr, which warn about text that is no number), an object it may convert, a class name it loads (method_exists($name, 'm'),is_callable('A::b'),defined('A::B'),class_alias($name, 'B'),new ReflectionClass($name),$arrayObject->setIteratorClass($name)), a class to construct ($statement->fetchObject('Row')) or a filter that one of PHP's ownFILTER_…constants does not name, which may be the one that calls back, or it runs what was registered earlier (an error, session or output-buffer handler, the callbacks of a cURL transfer or an XML parser, an iterator wrapped in another, a filter added to a stream). A warning that a PHP function raises by itself when called directly (hex2bingiven an odd number of digits, a file that cannot be read) is not counted, although the error handler of the project receives it. This limits every guard form of this specification, not only the ones the flow analysis reads.assert()is not a guard: assertions are compiled out in production configurations, so the read stays reported. Indirect evidence (a null-coalesced copy tested later) is not followed either.Exclude unresolved required symbols, user-defined lookalikes, incomplete syntax, unknown required values and unsupported versions.
An unpacked argument (
parse_url($url, ...$options)) may supplycomponent, which changes the result contract: no report.A property read is guarded like a variable when its earlier test and the read give the same value: the read runs no code (every class the receiver may be of declares the property without a hook, and has no
__get; a class extending it that declares the property again with a hook is not considered), and between them no call that may run project code, no write,unset, increment,foreachtarget or reference of a property of that name through any receiver, and no write of the receiver variable can change it; a loop around the read and not the test counts its whole body.Both
@custos-ignore ParseUrlFailureDereferencedand@noinspection ParseUrlFailureDereferencedsuppress this native rule.
Fix
None. A repair requires a semantic decision or application error-handling policy.
PHP versions
Available in PHP 5.3–8.5 where the referenced API exists. Do not apply extension contracts when the resolved API is unavailable. Gate syntax and version-specific behavior to the configured target.
Configure
In custos.json:
{
"rules": {
"ParseUrlFailureDereferenced": {
"enabled": false
}
}
}Suppress
Before the statement or declaration (or the first statement of the file):
// @custos-ignore ParseUrlFailureDereferenced