Statements and declarations¶
Rules for switch statements, ternaries and returns, for variables that are written and never read, and for how methods, properties and parameters are declared.
drupal/case-break-blank-line¶
- Level: error
- Fix: safe. It adds the line, or removes the extra ones.
- Standard: Control structures
- Ports:
Squiz.ControlStructures.SwitchDeclaration.SpacingAfterBreak
The standard doesn't have a sentence for this. Its switch example puts a blank line after the
break that ends each case.
Reports:
- a
break,continue,return,throw,exit,dieorgotothat ends a switch case and isn't followed by exactly one blank line. A comment on the statement's line doesn't count, and one on a later line does.
Not reported:
- the last case before the closing brace or
endswitch, and adefaultcase; - a case that falls through from a
default.
Example
The fix turns the reported code into the fine code.
// Reported, on the first break.
switch ($op) {
case 'view':
$access = TRUE;
break;
case 'edit':
$access = FALSE;
break;
default:
$access = NULL;
}
// Fine.
switch ($op) {
case 'view':
$access = TRUE;
break;
case 'edit':
$access = FALSE;
break;
default:
$access = NULL;
}
drupal/case-fall-through¶
- Level: error
- Fix: none
- Source: Coder's
Drupalstandard - Ports:
PSR2.ControlStructures.SwitchDeclaration.TerminatingComment
A case that runs on into the next one needs a comment there, so a reader can tell that the missing
break is on purpose.
Reports:
- a non-empty
casethat falls through to the next case with no comment right before that case. A comment of any kind counts, whatever it says, except a directive such as// @mago-expector// phpcs:ignore.
Not reported:
- a case that a
break,continue,return,throw,exit,dieorgotoends; - a case whose last statement ends it in every branch: an
ifwith anelse, atrywith itscatchblocks or with afinallyblock, or a nestedswitchwith adefaultcase; - a
defaultcase, and the last case.
Example
// Reported, on the page case.
switch ($type) {
case 'page':
$count++;
case 'article':
$count++;
break;
}
// Fine.
switch ($type) {
case 'page':
$count++;
// Pages count twice.
case 'article':
$count++;
break;
}
Compared with Coder
- A case that ends in
$x or exit()or$x ?? throw ...falls through when the left side allows it, and the rule reports it. Coder ends the case at the keyword. - A braceless
if (...) return; else return;ends a case. Coder reports it. - A case whose body is a
{ }block with no ending statement is reported. Coder stops with an internal exception for the whole file. - A
breakinside a nestedswitchdoesn't end the outer case, as in Coder. - Coder accepts a directive such as
// phpcs:ignoreas the comment. The rule doesn't.
drupal/case-label-line¶
- Level: error
- Fix: safe. It puts the label on the next line, at the indent of the comment's line.
- Standard: Control structures
- Ports:
Squiz.ControlStructures.SwitchDeclaration:ContentBeforeCase,ContentBeforeDefault
The standard doesn't have a sentence for this. Its switch example puts each case and default
label at the start of its line.
Reports:
- a block comment or a docblock that ends on the line of a
caseordefaultlabel, before the label. The rule reads both the brace and the colon syntax.
Example
The fix turns the reported code into the fine code.
// Reported.
switch ($op) {
/* Read access. */ case 'view':
$access = TRUE;
break;
default:
$access = FALSE;
}
// Fine.
switch ($op) {
/* Read access. */
case 'view':
$access = TRUE;
break;
default:
$access = FALSE;
}
Compared with Coder
- Coder reports any code before a label too.
mago formatmoves that code to a line of its own, so the rule reports only a comment. - When code or another comment comes before the comment on its line, the fix moves the comment
to a line of its own too.
mago formatputs the label back on the comment's line otherwise. phpcbf only moves the label. - Before a label that isn't the first one,
mago formatindents the comment as the case body above it, and Coder reports that line asDrupal.WhiteSpace.ScopeIndent.IncorrectExact.
drupal/case-semicolon¶
- Level: error
- Fix: safe. It writes the colon, and removes the blank space in front of the semicolon. A comment in front of it stays.
- Standard: Control structures
- Ports:
PSR2.ControlStructures.SwitchDeclaration:WrongOpenercase(partly),WrongOpenerdefault(partly)
The standard doesn't have a sentence for this. Its switch example ends each case and default
label with a colon.
Reports:
- a
caseordefaultlabel that ends with a semicolon, as incase 1;. The rule reads both the brace and the colon syntax, and checks the labels of a nested switch with that switch.
Compared with Coder
- Core's config runs only
WrongOpenercase, so core's phpcs doesn't reportdefault;. The rule reports it in core too. - Mago's parser doesn't accept a label that ends with a close tag, as in
case 1 ?>, which PHP accepts. Coder reports those, and the rule can't. Mago reports a parse error there and leaves that switch out, but still checks the rest of the file. - The fix writes the colon and removes the blank space in front of it in one pass. phpcbf
needs a second pass for the space, which the sniff's
SpaceBeforeColonCASEorSquiz.ControlStructures.SwitchDeclaration.SpaceBeforeColonCaseremoves. The result is the same.
drupal/comment-in-expression¶
- Level: error
- Fix: none
- Source: Coder's
Drupalstandard - Ports:
Generic.Formatting.SpaceAfterCast.CommentFoundGeneric.WhiteSpace.LanguageConstructSpacing.IncorrectYieldFromWithComment
Coder doesn't allow a comment inside a cast expression or between yield and from.
Reports:
- a comment right after a cast, as in
(int) /* note */ $x. Every cast spelling counts,(void)included, whatever the target PHP version; - a comment between
yieldandfrom.
Not reported:
- a comment before the cast, after the operand or after
from.
drupal/else-if¶
- Level: error
- Fix: safe. It joins the keywords.
- Standard: Control structures
- Ports:
PSR2.ControlStructures.ElseIfDeclaration.NotAllowed
(Note: Don't use "else if" -- always use elseif.)
Reports:
- an
else ifwritten as two keywords.
Not reported:
- a braced
else { if ... }.
Compared with Coder
- Coder reports
else ifas a warning. The rule reports it as an error.
drupal/empty-switch¶
- Level: error
- Fix: none
- Source: Coder's
Drupalstandard - Ports:
Squiz.ControlStructures.SwitchDeclaration.MissingCase
Coder asks every switch to have at least one case label.
Reports:
- a switch with no
caselabel. A switch that holds onlydefaultcounts, and so does an empty one. Acaselabel of a nested switch doesn't count for the outer switch.
Example
// Reported.
switch ($op) {
default:
$access = FALSE;
}
// Fine.
$access = FALSE;
Compared with Coder
- The rule reads the labels in the switch's own body. Coder walks tokens, so it needs special cases for nested switches.
- A file that doesn't parse doesn't get a report. Coder reports a statement placed directly in a switch body, which is invalid PHP.
drupal/explicit-nullable-param¶
- Level: error
- Fix: safe. It writes
?Tfor one type andA|B|nullfor a union. For an intersection, it writes(A&B)|nullwhenphp-versionis 8.2 or later, and nothing below 8.2. - Source: Coder's
Drupalstandard - Ports:
SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue.NullabilityTypeMissing
A null default makes a parameter's type nullable even when the type doesn't say so, as in
int $id = NULL. PHP 8.4 deprecates that. The fix writes the type that PHP already uses, so the
code runs the same. Its forms are the ones mago format writes with the drupal preset.
Mago's own explicit-nullable-param reports the same parameters, but it runs only when
php-version is 8.4 or later. This rule runs below 8.4 and stays silent from 8.4, so only one of
them reports a parameter. The issue code changes with php-version, so a @mago-expect comment or
a baseline entry written for one code doesn't cover the other.
Reports:
- a parameter of a function, method, closure or arrow function that has a type and a
nulldefault, in any case and with or without a leading backslash, when the type isn't?T, isn'tmixedand doesn't includenull. A parameter passed by reference counts.
Not reported:
- a parameter with no type;
- any parameter when
php-versionis 8.4 or later. Mago's rule reports it there, except for a\nulldefault.
Example
The fix turns the reported code into the fine code.
// Reported.
function example_load(int $id = NULL, int|string $key = NULL): void {
}
// Fine.
function example_load(?int $id = NULL, int|string|null $key = NULL): void {
}
Compared with Coder
- For an intersection, phpcbf writes
?A&B, which PHP doesn't parse. The fix writes(A&B)|nullfrom PHP 8.2, and leaves the type alone below 8.2.
drupal/function-blank-line¶
- Level: error
- Fix: safe. It adds a blank line above the function's docblock.
- Source: Coder's
Drupalstandard - Ports:
Squiz.WhiteSpace.FunctionSpacing.Before
Coder asks for a blank line between a function and the code or comment before it. mago format
writes that line after a use, namespace or declare statement, a class and another function,
and keeps one that is there. It doesn't add one after a plain statement, such as a define() call
or the } of an if.
Reports:
- a function declared after a statement or a comment with no blank line between them. The line goes above the function's docblock and attributes. As in Coder, only the nearest docblock counts, so a second docblock above it needs the blank line too.
Not reported:
- a function that is first in a block, after the
{or the:that opens it.mago formatremoves a blank line there; - a function right below a
//,#or/* */comment, as in Coder; - a function after another function, which
mago formatseparates; - a function at the start of a PHP block that doesn't start the file, as in Coder;
- a method, which
mago formatseparates.
The fix is left out when the function starts on the line of the code before it. mago format moves
the function to a line of its own, and the next fix adds the blank line.
Example
The fix turns the reported code into the fine code.
// Reported.
define('EXAMPLE_LIMIT', 10);
/**
* Returns the limit.
*/
function example_limit(): int {
return EXAMPLE_LIMIT;
}
// Fine.
define('EXAMPLE_LIMIT', 10);
/**
* Returns the limit.
*/
function example_limit(): int {
return EXAMPLE_LIMIT;
}
Compared with Coder
- Coder counts the blank line from a
//comment right above the docblock, and DrupalPractice'sCommentEmptyLine.SpacingAfterthen reports a blank line below that comment. A layout can't satisfy both. The rule followsFunctionSpacing, anddrupal/inline-comment-blank-lineallows a blank line between a comment and a docblock. - A run of comment lines right above the docblock that holds a tool comment, such as
// @mago-expector// phpcs:ignore, counts with the docblock, so the fix keeps the tool comment next to the function. Coder counts from the tool comment and reports the function again. - Coder also reports a function that is first in a
casebody or in a block of the alternative syntax, such asif ($a):.mago formatremoves the blank line there, so the rule doesn't report it.
drupal/inequality-operator¶
- Level: error
- Fix: safe
- Standard: Operators
- Ports: none. Coder doesn't check it.
Checks for weak-typed inequality MUST use the
!=operator. The<>operator MUST NOT be used in PHP code.
Reports:
- the
<>operator. The two spellings are the same operator to PHP, so the fix only changes the text.
Example
The fix turns the reported code into the fine code.
// Reported.
$changed = $old <> $new;
// Fine.
$changed = $old != $new;
drupal/method-visibility¶
- Level: error
- Fix: safe. It adds
public. - Standard: Methods and functions
- Ports:
Drupal.Scope.MethodScope.Missing
Visibility must be declared on all methods.
Reports:
- a method declared without
public,protectedorprivate.
drupal/non-capturing-catch¶
- Level: error
- Fix: safe. It removes the variable.
- Source: core's
phpcs.xml.dist - Ports: core's
SlevomatCodingStandard.Exceptions.RequireNonCapturingCatch, which is left out of Coder's rulesets.
From PHP 8.0, a catch clause can leave out its variable. A variable that's never read only makes
it look like the code uses the exception.
Reports:
- a
catchvariable whose name doesn't appear in the catch block, in thefinallyblock, or after the wholetrystatement in the same function or file, whenphp-versionis 8.0 or later. A name in a closure'suselist, in an arrow function and in an interpolated string counts. A name inside a nested function, closure body or class doesn't count, since those have variables of their own.
Not reported:
- a function that may read the variable by a name built at runtime, through
compact(),get_defined_vars()or a variable variable. The fix would remove a variable that it reads; - a name that appears anywhere in a
for,foreach,whileordoloop around thetry, other than as anothercatchvariable. The next pass of the loop reads it; - a name that shares its value with code outside the function: a
&$eparameter, a closure'suse (&$e)list, aglobal $eorstatic $edeclaration,&$eas in$x = &$eorforeach ($items as &$e), and$e = &$x. The catch writes through it. A variadic&...$eparameter doesn't count, since the catch replaces the array of references.
drupal/unused-variable allows an unused catch variable, as Coder's
UnusedVariable does, so this rule is the one that reports it.
Example
The fix turns the reported code into the fine code.
// Reported.
function example_load(string $path): ?string {
try {
return file_get_contents($path);
}
catch (\RuntimeException $e) {
return NULL;
}
}
// Fine.
function example_load(string $path): ?string {
try {
return file_get_contents($path);
}
catch (\RuntimeException) {
return NULL;
}
}
Compared with the sniff
- The sniff doesn't skip a function that calls
compact(), so its fix can remove a variable thatcompact()reads. - The sniff reports a name that a loop around the
tryreads before it, and a name that shares its value through a reference,globalorstatic. Its fix then changes what the code does.
drupal/null-coalesce¶
- Level: error
- Fix: safe for
isset(X) ? X : Bwhen X reads at most one property or index, as its last step, and for a null check when X is a variable, a constant or a static property. Potentially unsafe forisset(X) ? X : Bwhen X reads a property or an index other than as its last step, for a null check when X reads a property or an index anywhere in it, and when the fix drops a comment. - Standard: Operators
- Ports:
SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator.NullCoalesceOperatorNotUsed
The null coalescing operator
??should be used instead of a ternary operator with an isset() condition, to make code more readable.
Reports:
- a ternary that
??replaces, withnullon either side of the comparison:isset(X) ? X : BX === null ? B : XX !== null ? X : B
Operands match by syntax tree, so quotes, spacing and parentheses around an operand don't matter.
A condition in parentheses, as in (isset(X)) ? X : B, isn't reported, as with the sniff.
mago format removes those parentheses, and the ternary is then reported.
The fix writes X ?? B when X is a plain variable, property, index or constant read and B doesn't
need parentheses after ??. isset() and ?? both run __isset and then __get on an object,
or offsetExists and then offsetGet, so the isset shape keeps its result when X is a variable,
a constant or a static property, or one of those with one step, as in $a->b, $a['k'] or
self::$cache['k']. With two steps or more, as in $o->data['value'] or $a['k']['j'], or a
property or an index read inside an index, as in $a[$b->c], isset() runs every read but the
last one, and X runs them again. ?? runs them once, so __get or offsetGet can give a
different result, and the fix of the isset shape is potentially unsafe there. A null check doesn't run
__isset or offsetExists, and ?? does. When X reads a property, as $a->b or $a?->b, or
an index, as $a['k'], anywhere in it, an object with those methods can give a different result,
so the fix of a null check is potentially unsafe there. A static property, such as self::$cache,
doesn't have such methods. These are reported without a fix:
- an X that is a call, a variable variable such as
$$a, or a property with its name in braces, such as$a->{$b}; - a side effect in an index, or a cast before
isset; - a B without parentheses that is a ternary, an assignment, or a
throw,print,yield,includeorrequireexpression.
Example
The fix turns the reported code into the fine code.
// Reported.
$title = isset($form['title']) ? $form['title'] : '';
$name = $account === NULL ? 'Anonymous' : $account;
// Fine.
$title = $form['title'] ?? '';
$name = $account ?? 'Anonymous';
Compared with Coder
- The rule also reports a ternary after
and,orandxor. The formatter adds parentheses there, and the sniff then reports it. - It also reports a parenthesized operand, operands that differ only in quotes, and an
issetwhose key holds a call with a comma, such asisset($a[max(1, 2)]). The sniff skips all of these. It ignores spacing and comments in the operands, but not quotes or parentheses, and it skips anissetwith any comma. - It also reports a
nullcheck withnullfirst and an operand longer than one token, such asNULL === $a['k'] ? '' : $a['k']. The sniff reads only the token after===as the operand. - It doesn't report
!$a === null ? '' : $a,(string) $a === null ? '' : $aor$b + $a === null ? '' : $a. The compared operand is!$a,(string) $aor$b + $a, and a fix would change the result. - It doesn't report
$a === null ? '' : $a ?? 'z', whose else part is$a ?? 'z', or an(array)or(object)cast beforeisset, which always givestrue. - It also reports
X === null ? B : Xwhere B is a ternary without parentheses, such as$a === null ? $b ?: 1 : $a, and reports B as well when B matches. The sniff reports neither. - The sniff's fix drops comments and casts without notice. Here a comment makes the fix potentially unsafe, and a cast doesn't get a fix.
drupal/parameter-blank-line¶
- Level: error
- Fix: safe. It removes the line and keeps the line endings.
- Source: Coder's
Drupalstandard - Ports:
Drupal.Functions.MultiLineFunctionDeclaration.EmptyLine
Coder doesn't allow blank lines in a parameter list that spans lines. The standard's format for such a list puts one parameter on each line, and doesn't mention blank lines.
Reports:
- a blank line, or a line with only spaces, in the declaration of a function, method or closure
whose parameter list spans lines. The check covers the lines between the parentheses and, for a
closure, the
uselist, which counts only when the parameter list spans lines.
Not reported:
- a blank line inside a default value that holds an array, a call, parentheses or a string;
- a blank line inside an attribute or a comment;
- an arrow function.
drupal/inline-comment-blank-line also reports a blank line below a // comment in a parameter
list, with a fix for the same line. When both rules run in one pass, Mago skips one edit, and the
next pass applies it.
Example
The fix turns the reported code into the fine code.
// Reported.
function example_build(
string $id,
array $options,
): array {
return [];
}
// Fine.
function example_build(
string $id,
array $options,
): array {
return [];
}
drupal/property-per-statement¶
- Level: error
- Fix: none
- Source: Coder's
Drupalstandard - Ports:
PSR2.Classes.PropertyDeclaration.Multiple
Coder asks for one property in each declaration statement.
Reports:
- a statement that declares more than one property, as in
public $a, $b;. The report is on the first name.
There isn't a fix, because a split would have to sort out attributes, the docblock and the comments between the names.
Compared with Coder
- The rule reads the names of the declaration. Coder looks for the next variable before the next
semicolon, so it reports a property hook that reads
$thisor takes a$valueparameter, and a property that is followed by a hook with no semicolon in it. The rule doesn't report any of those, because a hooked property has one name. - Coder skips a property in an enum. The rule doesn't look at the enclosing type, so it also reports a multi-name statement in an enum, which is rejected by PHP and reported by Mago as well.
- It also reports a multi-name statement whose default isn't a constant expression. The sniff reports the same, and Mago reports that default on its own.
drupal/property-visibility¶
- Level: error
- Fix: safe. It writes
publicin place ofvar, or adds it, which is what PHP makes such a property. - Standard: Properties
- Ports:
Drupal.Classes.PropertyDeclaration.VarUsedPSR2.Classes.PropertyDeclaration.ScopeMissing
Visibility must be declared on all properties.
Reports:
- a property declared with
var; - a property declared without
public,protectedorprivate, such asstatic $count;.
Compared with Coder
- A
varproperty is reported once. Coder 9 also reports the missing visibility. - A fix for a property declared without a visibility doesn't exist in phpcbf.
- Coder reports a missing visibility on
$thisor a$valueparameter inside a property hook. The rule reads only the declared names, so it doesn't.
drupal/redundant-return¶
- Level: warning
- Fix: safe. It removes the statement, and the line when the statement is alone on it. There
isn't a fix when a comment sits inside the statement, such as
return /* note */;. - Source: Coder's
Drupalstandard - Ports:
Squiz.PHP.NonExecutableCode.ReturnNotRequired
A function ends the same way without a return; as its last statement.
Reports:
- a
return;that is the last statement of a function, method or closure body, or of a{ }block that ends the body.
Not reported:
- a
return;in anif, loop,tryorswitch; - a
returnwith a value; - a
return;that code follows.
Example
The fix turns the reported code into the fine code.
// Reported.
function example_reset(array &$cache): void {
$cache = [];
return;
}
// Fine.
function example_reset(array &$cache): void {
$cache = [];
}
Compared with Coder
- The rule skips a
return;that is the whole body of an unbraced loop, because removing it changes the loop. Coder reports it. - It reports a
return;that ends a{ }block at the end of a body. Coder stops with an internal exception there and loses the rest of the file's reports. - A fix for the statement doesn't exist in Coder.
drupal/short-ternary¶
- Level: error
- Fix: safe when X is a variable, a constant or a static property. Potentially unsafe when X reads a property or an index anywhere in it, and when the fix drops a comment.
- Standard: Operators
- Ports: core's
SlevomatCodingStandard.ControlStructures.RequireShortTernaryOperator, which is left out of Coder's rulesets.
The short ternary operator
?:must be used where the first operand of a ternary expression matches the condition.
Reports:
- a ternary that repeats its condition, which
?:replaces:X ? X : B!X ? B : X
Both become X ?: B. Operands match by syntax tree, as in
drupal/null-coalesce, so quotes, spacing, comments and parentheses don't
matter. A condition in parentheses, as in (X) ? X : B, isn't reported, as with the sniff, unless
the middle part has parentheses too. mago format removes those parentheses, and the ternary is
then reported.
The fix runs when X is a plain variable, property, index or constant read, since X ?: B reads X
once where the ternary reads it twice. In the second shape B moves to the end, so it also needs to
bind tighter than ??, as in drupal/null-coalesce. Other matches are reported without a fix.
When X reads a property, as $a->b or $a?->b, or an index, as $a['k'], anywhere in it, the
fix is potentially unsafe: on an object, __get or offsetGet then runs once instead of twice. A
static property, such as self::$cache, doesn't have such methods.
Mago's own no-shorthand-ternary asks for the opposite. The
configuration turns it
off.
Example
The fix turns the reported code into the fine code.
// Reported.
$label = $title ? $title : 'Untitled';
$name = !$nickname ? $username : $nickname;
// Fine.
$label = $title ?: 'Untitled';
$name = $nickname ?: $username;
Compared with the sniff
- The sniff compares the source text, so it skips operands that differ in quotes, spacing or comments. The rule reports them.
- The sniff stops reading the condition at a comma, so it skips a condition such as
foo($a, $b) ? foo($a, $b) : 'x'. The rule reports it.
drupal/short-list¶
- Level: error
- Fix: safe. It writes
[...]and removes the blank space betweenlistand the parenthesis. It is potentially unsafe when it would drop a comment there. An emptylist()doesn't have a fix. - Source: Coder's
Drupalstandard - Ports:
SlevomatCodingStandard.PHP.ShortList.LongListUsed
Coder asks for the short [...] form for destructuring, as for arrays.
Reports:
- a destructuring written as
list(...), in an assignment, aforeachor a nested position. The rule reports everylistkeyword, in any letter case.
Not reported:
- a name that only looks like the keyword, which isn't a destructuring:
->list,::list, a method, constant, property or enum case calledlist, a named argument, and text in a string or a comment.
Compared with Coder
- Coder's fix drops a comment between
listand the parenthesis. Here such a fix is potentially unsafe, and a plain--fixleaves it. - Coder fixes an empty
list()into[], which PHP rejects as well. The rule reports it and doesn't offer a fix.
drupal/unused-variable¶
- Level: warning
- Fix: none
- Source: Coder's
DrupalPracticestandard - Ports:
VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
A variable that is written and never read is dead code, or the code reads another name by mistake. The check works per scope and doesn't follow the order of the code: a scope that reads a name anywhere uses it. A scope is a function, a method, a closure or the top level of a file. An arrow function uses the scope around it, except for the names of its parameters.
Reports:
- every write of a name that its scope never reads. A write is the left side of an assignment,
compound ones such as
.=and??=included, an element of a list that is assigned, aforeachkey, aforeachvalue without a key, astaticdeclaration, and aglobaldeclaration that the function never assigns; - a variable of a closure's
uselist that the closure never reads, and every write to it in the closure.
Every other use is a read, such as $count++, $items[] = $item, $node->title = $title,
isset($x) and unset($x). A name in an interpolated string or a heredoc and a name in compact()
are reads too, and so is every variable that the scope used before a get_defined_vars() call. An
assignment that is the value of another assignment, or an argument of a call, also counts as a read,
as in $a = $b = 1 and example_save($id = 5).
Not reported:
- a parameter, also one that the function assigns;
- a
catchvariable, and other writes of its name in the same scope.drupal/non-capturing-catchreports one that's never read; - the value of a
$key => $valueforeach, and other writes of its name in the same scope; $this, the superglobals,$argv,$argc,$http_response_headerand$HTTP_RAW_POST_DATA;- a variable of a
use (&$x)list; - a reference that the code writes through: a
$ref = &$valuebinding that a$ref = ...assignment writes through, and aforeach ($items as &$item)value that an$item = ...assignment writes through; - a property, and a static property such as
self::$count; - a
*.tpl.phpfile, which Coder's ruleset leaves out.
Mago's own no-redundant-variable reports a value that isn't read later, which is a different
check. The configuration turns it off.
Example
// Reported, on $count and on $key.
function example_names(array $items): array {
$count = count($items);
$names = [];
foreach ($items as $key => $item) {
$names[] = $item['name'];
}
return $names;
}
// Fine.
function example_names(array $items): array {
$names = [];
foreach ($items as $item) {
$names[] = $item['name'];
}
return $names;
}
Compared with Coder
- A read in an
elseorelseifbranch counts. Coder skips it when every earlier write of the name is in the branches above it, so it also reports a value that a loop writes in theifbranch and reads in theelsebranch on a later pass. - A
= &binding that walks down an array, as$target = &$target[$key]in a loop, reads$target. Coder reports the binding before it. - Coder treats a
foreachvariable as the value of a$key => $valuepair when any=>comes before it inside the parentheses, the looped array included. So it never reports the key inforeach (['a' => 1] as $key => $value), the value inforeach (['a' => 1] as $value), or the list elements inforeach (['k' => [1, 2]] as [$a, $b])andforeach ($rows as ['x' => $x]). The rule reports each of them when it's never read. - In
fn($a) => fn($b) => $a + $b, the inner$areads the outer arrow function's parameter. Coder counts it as a read of an$aaround the arrow functions. compact(['name'])reads$name. Coder reads only string arguments andarray(...).- The array in
[$map[$key]] = $pairand the object inforeach ($items as $object->value)are read, as in any write to an index or a property. Coder counts them as written. - An assignment in a later element of an array that another assignment holds, as
$x = ['a' => 1, 'b' => $y = 2], counts as a read. Coder counts it as a read only in the first element.
drupal/variable-redeclaration¶
- Level: warning
- Fix: none
- Source: Coder's
DrupalPracticestandard - Ports:
VariableAnalysis.CodeAnalysis.VariableAnalysis.VariableRedeclaration
A function that declares a name a second time usually overwrites a value by mistake, such as a
parameter that a static or global declaration replaces. PHP rejects some of these shapes: a
repeated parameter, a closure's use variable that repeats a parameter, and, from PHP 8.3, a
repeated static declaration. The rule compares the declarations of each name in source order, in
the scopes of drupal/unused-variable, so a declaration in another branch
counts too.
Reports:
- a parameter declared again by
static,global, acatchvariable, a= &binding, a closure'suselist or another parameter; - a variable that the scope wrote before, then declared
globalorstatic; - a
globalorstaticdeclaration of a name that the scope already declared asglobalorstatic; - a
catchvariable or a= &binding of a name declared asglobal,staticor a closure'susevariable.
A write is an assignment, a foreach variable or an element of a list that is assigned.
Not reported:
- a
catchvariable or a= &binding of a name that the scope only wrote, or bound before; static $x = 1;, which Coder treats as an assignment, not as a declaration;- a name that the scope only read before the declaration;
- a
*.tpl.phpfile, which Coder's ruleset leaves out.
A rebinding on purpose, such as $elements = &$form; over a parameter, needs a
// @mago-expect lint:drupal/variable-redeclaration comment above it. Mago reports a repeated
parameter as a semantics error too.
Example
// Reported.
function example_counter(int $count): int {
static $count;
return $count ?? 0;
}
// Fine.
function example_counter(int $start): int {
static $count;
$count ??= $start;
return $count;
}
Compared with Coder
- Coder skips a closure's
usevariable that isn't defined before the closure, so it doesn't report one that repeats a parameter of the closure then. The rule reports it. - Coder counts a write through a by-reference argument, such as the
$matchesofpreg_match(), and$items[] = ...on a new variable as assignments, and reports aglobalorstaticdeclaration of the name after them. The rule counts both as uses of the name.