Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4080103
EmptyStatementListPlugin.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
12 KB
Referenced Files
None
Subscribers
None
EmptyStatementListPlugin.php
View Options
<?php
declare
(
strict_types
=
1
);
use
ast\Node
;
use
Phan\AST\InferPureAndNoThrowVisitor
;
use
Phan\Config
;
use
Phan\Library\FileCache
;
use
Phan\Parse\ParseVisitor
;
use
Phan\PluginV3
;
use
Phan\PluginV3\PluginAwarePostAnalysisVisitor
;
use
Phan\PluginV3\PostAnalyzeNodeCapability
;
/**
* This file checks for empty statement lists in loops/branches.
* Due to Phan's AST rewriting for easier analysis, this may miss some edge cases.
*
* It hooks into one event:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a class that is called on every AST node from every
* file being analyzed
*
* A plugin file must
*
* - Contain a class that inherits from \Phan\PluginV3
*
* - End by returning an instance of that class.
*
* It is assumed without being checked that plugins aren't
* mangling state within the passed code base or context.
*
* Note: When adding new plugins,
* add them to the corresponding section of README.md
*/
final
class
EmptyStatementListPlugin
extends
PluginV3
implements
PostAnalyzeNodeCapability
{
/**
* If true, then never allow empty statement lists, even if there is a TODO/FIXME/"deliberately empty" comment.
* @var bool
* @internal
*/
public
static
$ignore_todos
=
false
;
public
function
__construct
()
{
self
::
$ignore_todos
=
(
bool
)
(
Config
::
getValue
(
'plugin_config'
)[
'empty_statement_list_ignore_todos'
]
??
false
);
}
/**
* @return string - The name of the visitor that will be called.
*/
public
static
function
getPostAnalyzeNodeVisitorClassName
():
string
{
return
EmptyStatementListVisitor
::
class
;
}
}
/**
* When __invoke on this class is called with a node, a method
* will be dispatched based on the `kind` of the given node.
*
* Visitors such as this are useful for defining lots of different
* checks on a node based on its kind.
*/
final
class
EmptyStatementListVisitor
extends
PluginAwarePostAnalysisVisitor
{
/**
* @var list<Node> set by plugin framework
* @suppress PhanReadOnlyProtectedProperty
*/
protected
$parent_node_list
;
/**
* @param Node $node
* A node to analyze
* @override
*/
public
function
visitIf
(
Node
$node
):
void
{
// @phan-suppress-next-line PhanUndeclaredProperty set by ASTSimplifier
if
(
isset
(
$node
->
is_simplified
))
{
$first_child
=
end
(
$node
->
children
);
if
(!
$first_child
instanceof
Node
||
$first_child
->
children
[
'cond'
]
===
null
)
{
return
;
}
$last_if_elem
=
reset
(
$node
->
children
);
}
else
{
$last_if_elem
=
end
(
$node
->
children
);
}
if
(!
$last_if_elem
instanceof
Node
)
{
// probably impossible
return
;
}
$stmts_node
=
$last_if_elem
->
children
[
'stmts'
];
if
(!
$stmts_node
instanceof
Node
)
{
// probably impossible
return
;
}
if
(
$stmts_node
->
children
)
{
// the last if element has statements
return
;
}
if
(
$last_if_elem
->
children
[
'cond'
]
===
null
)
{
// Don't bother warning about else
return
;
}
if
(
$this
->
hasTODOComment
(
$stmts_node
->
lineno
,
$node
))
{
// Don't warn if there is a FIXME/TODO comment in/around the empty statement list
return
;
}
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$last_if_elem
->
children
[
'stmts'
]->
lineno
??
$last_if_elem
->
lineno
),
'PhanPluginEmptyStatementIf'
,
'Empty statement list detected for the last if/elseif statement'
,
[]
);
}
private
function
hasTODOComment
(
int
$lineno
,
Node
$analyzed_node
,
?
int
$end_lineno
=
null
):
bool
{
if
(
EmptyStatementListPlugin
::
$ignore_todos
)
{
return
false
;
}
$file
=
FileCache
::
getOrReadEntry
(
$this
->
context
->
getFile
());
$lines
=
$file
->
getLines
();
$end_lineno
=
max
(
$lineno
,
$end_lineno
??
$this
->
findEndLine
(
$lineno
,
$analyzed_node
));
for
(
$i
=
$lineno
;
$i
<=
$end_lineno
;
$i
++)
{
$line
=
$lines
[
$i
]
??
null
;
if
(!
is_string
(
$line
))
{
break
;
}
if
(
preg_match
(
'/todo|fixme|deliberately empty/i'
,
$line
)
>
0
)
{
return
true
;
}
}
return
false
;
}
private
function
findEndLine
(
int
$lineno
,
Node
$search_node
):
int
{
for
(
$node_index
=
count
(
$this
->
parent_node_list
)
-
1
;
$node_index
>=
0
;
$node_index
--)
{
$node
=
$this
->
parent_node_list
[
$node_index
]
??
null
;
if
(!
$node
)
{
continue
;
}
if
(
isset
(
$node
->
endLineno
))
{
// Return the end line of the function declaration.
return
$node
->
endLineno
;
}
if
(
$node
->
kind
===
ast\AST_STMT_LIST
)
{
foreach
(
$node
->
children
as
$i
=>
$c
)
{
if
(
$c
===
$search_node
)
{
$next_node
=
$node
->
children
[
$i
+
1
]
??
null
;
if
(
$next_node
instanceof
Node
)
{
return
$next_node
->
lineno
-
1
;
}
break
;
}
}
}
$search_node
=
$node
;
}
// Give up and guess.
return
$lineno
+
5
;
}
/**
* @param Node $node
* A node of kind ast\AST_FOR to analyze
* @override
*/
public
function
visitFor
(
Node
$node
):
void
{
$stmts_node
=
$node
->
children
[
'stmts'
];
if
(!
$stmts_node
instanceof
Node
)
{
// impossible
return
;
}
if
(
$stmts_node
->
children
||
(
$node
->
children
[
'loop'
]->
children
??
null
))
{
// the for loop has statements, in the body and/or in the loop condition.
return
;
}
if
(
$this
->
hasTODOComment
(
$stmts_node
->
lineno
,
$node
))
{
// Don't warn if there is a FIXME/TODO comment in/around the empty statement list
return
;
}
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$stmts_node
->
lineno
??
$node
->
lineno
),
'PhanPluginEmptyStatementForLoop'
,
'Empty statement list detected for the for loop'
,
[]
);
}
/**
* @param Node $node
* A node to analyze
* @override
*/
public
function
visitWhile
(
Node
$node
):
void
{
$stmts_node
=
$node
->
children
[
'stmts'
];
if
(!
$stmts_node
instanceof
Node
)
{
return
;
// impossible
}
if
(
$stmts_node
->
children
)
{
// the while loop has statements
return
;
}
if
(
$this
->
hasTODOComment
(
$stmts_node
->
lineno
,
$node
))
{
// Don't warn if there is a FIXME/TODO comment in/around the empty statement list
return
;
}
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$stmts_node
->
lineno
??
$node
->
lineno
),
'PhanPluginEmptyStatementWhileLoop'
,
'Empty statement list detected for the while loop'
,
[]
);
}
/**
* @param Node $node
* A node to analyze
* @override
*/
public
function
visitDoWhile
(
Node
$node
):
void
{
$stmts_node
=
$node
->
children
[
'stmts'
];
if
(!
$stmts_node
instanceof
Node
)
{
return
;
// impossible
}
if
(
$stmts_node
->
children
??
null
)
{
// the while loop has statements
return
;
}
if
(
$this
->
hasTODOComment
(
$stmts_node
->
lineno
,
$node
))
{
// Don't warn if there is a FIXME/TODO comment in/around the empty statement list
return
;
}
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$stmts_node
->
lineno
),
'PhanPluginEmptyStatementDoWhileLoop'
,
'Empty statement list detected for the do-while loop'
,
[]
);
}
/**
* @param Node $node
* A node to analyze
* @override
*/
public
function
visitForeach
(
Node
$node
):
void
{
$stmts_node
=
$node
->
children
[
'stmts'
];
if
(!
$stmts_node
instanceof
Node
)
{
// impossible
return
;
}
if
(
$stmts_node
->
children
)
{
// the while loop has statements
return
;
}
if
(
$this
->
hasTODOComment
(
$stmts_node
->
lineno
,
$node
))
{
// Don't warn if there is a FIXME/TODO comment in/around the empty statement list
return
;
}
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$stmts_node
->
lineno
),
'PhanPluginEmptyStatementForeachLoop'
,
'Empty statement list detected for the foreach loop'
,
[]
);
}
/**
* @param Node $node
* A node to analyze
* @override
*/
public
function
visitTry
(
Node
$node
):
void
{
[
'try'
=>
$try_node
,
'finally'
=>
$finally_node
]
=
$node
->
children
;
if
(!
$try_node
->
children
)
{
if
(!
$this
->
hasTODOComment
(
$try_node
->
lineno
,
$node
,
$node
->
children
[
'catches'
]->
children
[
0
]->
lineno
??
$finally_node
->
lineno
??
null
))
{
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$try_node
->
lineno
),
'PhanPluginEmptyStatementTryBody'
,
'Empty statement list detected for the try statement
\'
s body'
,
[]
);
}
}
elseif
(
InferPureAndNoThrowVisitor
::
isUnlikelyToThrow
(
$this
->
code_base
,
$this
->
context
,
$try_node
))
{
if
(!
$this
->
hasTODOComment
(
$try_node
->
lineno
,
$node
,
$node
->
children
[
'catches'
]->
children
[
0
]->
lineno
??
$finally_node
->
lineno
??
null
))
{
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$try_node
->
lineno
),
'PhanPluginEmptyStatementPossiblyNonThrowingTryBody'
,
'Found a try block that looks like it might not throw. Note that this check is a heuristic prone to false positives, especially because error handlers, signal handlers, destructors, and other things may all lead to throwing.'
);
}
}
if
(
$finally_node
instanceof
Node
&&
!
$finally_node
->
children
)
{
if
(!
$this
->
hasTODOComment
(
$finally_node
->
lineno
,
$node
))
{
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$finally_node
->
lineno
),
'PhanPluginEmptyStatementTryFinally'
,
'Empty statement list detected for the try
\'
s finally body'
,
[]
);
}
}
}
/**
* @param Node $node
* A node of kind ast\AST_SWITCH to analyze
* @override
*/
public
function
visitSwitch
(
Node
$node
):
void
{
// Check all case statements and return if something that isn't a no-op is seen.
foreach
(
$node
->
children
[
'stmts'
]->
children
??
[]
as
$c
)
{
if
(!
$c
instanceof
Node
)
{
// impossible
continue
;
}
$children
=
$c
->
children
[
'stmts'
]->
children
??
null
;
if
(
$children
)
{
if
(
count
(
$children
)
>
1
)
{
return
;
}
$only_node
=
$children
[
0
];
if
(
$only_node
instanceof
Node
)
{
if
(!
in_array
(
$only_node
->
kind
,
[
ast\AST_CONTINUE
,
ast\AST_BREAK
],
true
))
{
return
;
}
if
((
$only_node
->
children
[
'depth'
]
??
1
)
!==
1
)
{
// not a no-op
return
;
}
}
}
if
(!
ParseVisitor
::
isConstExpr
(
$c
->
children
[
'cond'
],
ParseVisitor
::
CONSTANT_EXPRESSION_FORBID_NEW_EXPRESSION
))
{
return
;
}
}
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$node
->
lineno
),
'PhanPluginEmptyStatementSwitch'
,
'No side effects seen for any cases of this switch statement'
,
[]
);
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return
new
EmptyStatementListPlugin
();
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Aug 18 2026, 05:25 (8 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
6f/8e/417234a3aea744edbefc9a5d6873
Default Alt Text
EmptyStatementListPlugin.php (12 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment