Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5969458
DuplicateExpressionPlugin.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
20 KB
Referenced Files
None
Subscribers
None
DuplicateExpressionPlugin.php
View Options
<?php
declare
(
strict_types
=
1
);
use
ast\flags
;
use
ast\Node
;
use
Phan\Analysis\PostOrderAnalysisVisitor
;
use
Phan\AST\ASTHasher
;
use
Phan\AST\ASTReverter
;
use
Phan\AST\InferValue
;
use
Phan\Config
;
use
Phan\PluginV3
;
use
Phan\PluginV3\PluginAwarePostAnalysisVisitor
;
use
Phan\PluginV3\PluginAwarePreAnalysisVisitor
;
use
Phan\PluginV3\PostAnalyzeNodeCapability
;
use
Phan\PluginV3\PreAnalyzeNodeCapability
;
/**
* This plugin checks for duplicate expressions in a statement
* that are likely to be a bug.
*
* - E.g. `expr1 == expr1`
*
* This file demonstrates plugins for Phan. Plugins hook into various events.
* DuplicateExpressionPlugin hooks into two events:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed in post-order
* - getPreAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed in pre-order
*
* 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
*/
class
DuplicateExpressionPlugin
extends
PluginV3
implements
PostAnalyzeNodeCapability
,
PreAnalyzeNodeCapability
{
/**
* @return class-string - name of PluginAwarePostAnalysisVisitor subclass
*/
public
static
function
getPostAnalyzeNodeVisitorClassName
():
string
{
return
RedundantNodePostAnalysisVisitor
::
class
;
}
/**
* @return class-string - name of PluginAwarePreAnalysisVisitor subclass
*/
public
static
function
getPreAnalyzeNodeVisitorClassName
():
string
{
return
RedundantNodePreAnalysisVisitor
::
class
;
}
}
/**
* This visitor analyzes node kinds that can be the root of expressions
* containing duplicate expressions, and is called on nodes in post-order.
*/
class
RedundantNodePostAnalysisVisitor
extends
PluginAwarePostAnalysisVisitor
{
/**
* These are types of binary operations for which it is
* likely to be a typo if both the left and right-hand sides
* of the operation are the same.
*/
private
const
REDUNDANT_BINARY_OP_SET
=
[
flags\BINARY_BOOL_AND
=>
true
,
flags\BINARY_BOOL_OR
=>
true
,
flags\BINARY_BOOL_XOR
=>
true
,
flags\BINARY_BITWISE_OR
=>
true
,
flags\BINARY_BITWISE_AND
=>
true
,
flags\BINARY_BITWISE_XOR
=>
true
,
flags\BINARY_SUB
=>
true
,
flags\BINARY_DIV
=>
true
,
flags\BINARY_MOD
=>
true
,
flags\BINARY_IS_IDENTICAL
=>
true
,
flags\BINARY_IS_NOT_IDENTICAL
=>
true
,
flags\BINARY_IS_EQUAL
=>
true
,
flags\BINARY_IS_NOT_EQUAL
=>
true
,
flags\BINARY_IS_SMALLER
=>
true
,
flags\BINARY_IS_SMALLER_OR_EQUAL
=>
true
,
flags\BINARY_IS_GREATER
=>
true
,
flags\BINARY_IS_GREATER_OR_EQUAL
=>
true
,
flags\BINARY_SPACESHIP
=>
true
,
flags\BINARY_COALESCE
=>
true
,
];
/**
* A subset of REDUNDANT_BINARY_OP_SET.
*
* These binary operations will make this plugin warn if both sides are literals.
*/
private
const
BINARY_OP_BOTH_LITERAL_WARN_SET
=
[
flags\BINARY_BOOL_AND
=>
true
,
flags\BINARY_BOOL_OR
=>
true
,
flags\BINARY_BOOL_XOR
=>
true
,
flags\BINARY_IS_IDENTICAL
=>
true
,
flags\BINARY_IS_NOT_IDENTICAL
=>
true
,
flags\BINARY_IS_EQUAL
=>
true
,
flags\BINARY_IS_NOT_EQUAL
=>
true
,
flags\BINARY_IS_SMALLER
=>
true
,
flags\BINARY_IS_SMALLER_OR_EQUAL
=>
true
,
flags\BINARY_IS_GREATER
=>
true
,
flags\BINARY_IS_GREATER_OR_EQUAL
=>
true
,
flags\BINARY_SPACESHIP
=>
true
,
flags\BINARY_COALESCE
=>
true
,
];
/**
* @param Node $node
* A binary operation node to analyze
* @override
* @suppress PhanAccessClassConstantInternal
*/
public
function
visitBinaryOp
(
Node
$node
):
void
{
$flags
=
$node
->
flags
;
if
(!
\array_key_exists
(
$flags
,
self
::
REDUNDANT_BINARY_OP_SET
))
{
// Nothing to warn about
return
;
}
$left
=
$node
->
children
[
'left'
];
$right
=
$node
->
children
[
'right'
];
if
(
ASTHasher
::
hash
(
$left
)
===
ASTHasher
::
hash
(
$right
))
{
$this
->
emitPluginIssue
(
$this
->
code_base
,
$this
->
context
,
'PhanPluginDuplicateExpressionBinaryOp'
,
'Both sides of the binary operator {OPERATOR} are the same: {CODE}'
,
[
PostOrderAnalysisVisitor
::
NAME_FOR_BINARY_OP
[
$node
->
flags
],
ASTReverter
::
toShortString
(
$left
),
]
);
return
;
}
if
(!
\array_key_exists
(
$flags
,
self
::
BINARY_OP_BOTH_LITERAL_WARN_SET
))
{
return
;
}
if
(
$left
instanceof
Node
)
{
$left
=
self
::
resolveLiteralValue
(
$left
);
if
(
$left
instanceof
Node
)
{
return
;
}
}
if
(
$right
instanceof
Node
)
{
$right
=
self
::
resolveLiteralValue
(
$right
);
if
(
$right
instanceof
Node
)
{
return
;
}
}
try
{
// @phan-suppress-next-line PhanPartialTypeMismatchArgument TODO: handle
$result_representation
=
ASTReverter
::
toShortString
(
InferValue
::
computeBinaryOpResult
(
$left
,
$right
,
$flags
));
}
catch
(
Error
$_
)
{
$result_representation
=
'(unknown)'
;
}
$this
->
emitPluginIssue
(
$this
->
code_base
,
$this
->
context
,
'PhanPluginBothLiteralsBinaryOp'
,
'Suspicious usage of a binary operator where both operands are literals. Expression: {CODE} {OPERATOR} {CODE} (result is {CODE})'
,
[
ASTReverter
::
toShortString
(
$left
),
PostOrderAnalysisVisitor
::
NAME_FOR_BINARY_OP
[
$flags
],
ASTReverter
::
toShortString
(
$right
),
$result_representation
,
]
);
}
/**
* @param Node $node
* An assignment operation node to analyze
* @override
*/
public
function
visitAssignRef
(
Node
$node
):
void
{
$this
->
visitAssign
(
$node
);
}
private
const
ASSIGN_OP_FLAGS
=
[
flags\BINARY_BITWISE_OR
=>
'|'
,
flags\BINARY_BITWISE_AND
=>
'&'
,
flags\BINARY_BITWISE_XOR
=>
'^'
,
flags\BINARY_CONCAT
=>
'.'
,
flags\BINARY_ADD
=>
'+'
,
flags\BINARY_SUB
=>
'-'
,
flags\BINARY_MUL
=>
'*'
,
flags\BINARY_DIV
=>
'/'
,
flags\BINARY_MOD
=>
'%'
,
flags\BINARY_POW
=>
'**'
,
flags\BINARY_SHIFT_LEFT
=>
'<<'
,
flags\BINARY_SHIFT_RIGHT
=>
'>>'
,
flags\BINARY_COALESCE
=>
'??'
,
];
/**
* @param Node $node
* An assignment operation node to analyze
* @override
*/
public
function
visitAssign
(
Node
$node
):
void
{
$expr
=
$node
->
children
[
'expr'
];
if
(!
$expr
instanceof
Node
)
{
// Guaranteed not to contain duplicate expressions in valid php assignments.
return
;
}
$var
=
$node
->
children
[
'var'
];
if
(
$expr
->
kind
===
ast\AST_BINARY_OP
)
{
$op_str
=
self
::
ASSIGN_OP_FLAGS
[
$expr
->
flags
]
??
null
;
if
(
is_string
(
$op_str
)
&&
ASTHasher
::
hash
(
$var
)
===
ASTHasher
::
hash
(
$expr
->
children
[
'left'
]))
{
$message
=
'Can simplify this assignment to {CODE} {OPERATOR} {CODE}'
;
if
(
$expr
->
flags
===
ast\flags\BINARY_COALESCE
)
{
if
(
Config
::
get_closest_minimum_target_php_version_id
()
<
70400
)
{
return
;
}
$message
.=
' (requires php version 7.4 or newer)'
;
}
$this
->
emitPluginIssue
(
$this
->
code_base
,
$this
->
context
,
'PhanPluginDuplicateExpressionAssignmentOperation'
,
$message
,
[
ASTReverter
::
toShortString
(
$var
),
$op_str
.
'='
,
ASTReverter
::
toShortString
(
$expr
->
children
[
'right'
]),
]
);
}
return
;
}
if
(
ASTHasher
::
hash
(
$var
)
===
ASTHasher
::
hash
(
$expr
))
{
$this
->
emitPluginIssue
(
$this
->
code_base
,
$this
->
context
,
'PhanPluginDuplicateExpressionAssignment'
,
'Both sides of the assignment {OPERATOR} are the same: {CODE}'
,
[
$node
->
kind
===
ast\AST_ASSIGN_REF
?
'=&'
:
'='
,
ASTReverter
::
toShortString
(
$var
),
]
);
return
;
}
}
/**
* @return bool|null|Node the resolved value of $node, or $node if it could not be resolved
* This could be more permissive about what constants are allowed (e.g. user-defined constants, real constants like PI, etc.),
* but that may cause more false positives.
*/
private
static
function
resolveLiteralValue
(
Node
$node
)
{
if
(
$node
->
kind
!==
ast\AST_CONST
)
{
return
$node
;
}
// @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal
switch
(
\strtolower
(
$node
->
children
[
'name'
]->
children
[
'name'
]
??
null
))
{
case
'false'
:
return
false
;
case
'true'
:
return
true
;
case
'null'
:
return
null
;
default
:
return
$node
;
}
}
/**
* @param Node $node
* A binary operation node to analyze
* @override
*/
public
function
visitConditional
(
Node
$node
):
void
{
$cond_node
=
$node
->
children
[
'cond'
];
$true_node_hash
=
ASTHasher
::
hash
(
$node
->
children
[
'true'
]);
if
(
ASTHasher
::
hash
(
$cond_node
)
===
$true_node_hash
)
{
$this
->
emitPluginIssue
(
$this
->
code_base
,
$this
->
context
,
'PhanPluginDuplicateConditionalTernaryDuplication'
,
'"X ? X : Y" can usually be simplified to "X ?: Y". The duplicated expression X was {CODE}'
,
[
ASTReverter
::
toShortString
(
$cond_node
)]
);
return
;
}
$false_node_hash
=
ASTHasher
::
hash
(
$node
->
children
[
'false'
]);
if
(
$true_node_hash
===
$false_node_hash
)
{
$this
->
emitPluginIssue
(
$this
->
code_base
,
$this
->
context
,
'PhanPluginDuplicateConditionalUnnecessary'
,
'"X ? Y : Y" results in the same expression Y no matter what X evaluates to. Y was {CODE}'
,
[
ASTReverter
::
toShortString
(
$cond_node
)]
);
return
;
}
if
(!
$cond_node
instanceof
Node
)
{
return
;
}
switch
(
$cond_node
->
kind
)
{
case
ast\AST_ISSET
:
if
(
ASTHasher
::
hash
(
$cond_node
->
children
[
'var'
])
===
$true_node_hash
)
{
$this
->
warnDuplicateConditionalNullCoalescing
(
'isset(X) ? X : Y'
,
$node
->
children
[
'true'
]);
}
break
;
case
ast\AST_BINARY_OP
:
$this
->
checkBinaryOpOfConditional
(
$cond_node
,
$true_node_hash
);
break
;
case
ast\AST_UNARY_OP
:
$this
->
checkUnaryOpOfConditional
(
$cond_node
,
$true_node_hash
);
break
;
}
}
/**
* @param Node $node
* A statement list of kind ast\AST_STMT_LIST to analyze.
* @override
*/
public
function
visitStmtList
(
Node
$node
):
void
{
$children
=
$node
->
children
;
if
(
count
(
$children
)
<
2
)
{
return
;
}
$prev_hash
=
null
;
foreach
(
$children
as
$child
)
{
$hash
=
ASTHasher
::
hash
(
$child
);
if
(
$hash
===
$prev_hash
)
{
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$child
->
lineno
??
$node
->
lineno
),
'PhanPluginDuplicateAdjacentStatement'
,
"Statement {CODE} is a duplicate of the statement on the above line. Suppress this issue instance if there's a good reason for this."
,
[
ASTReverter
::
toShortString
(
$child
)]
);
}
$prev_hash
=
$hash
;
}
}
/**
* @param int|string $true_node_hash
*/
private
function
checkBinaryOpOfConditional
(
Node
$cond_node
,
$true_node_hash
):
void
{
if
(
$cond_node
->
flags
!==
ast\flags\BINARY_IS_NOT_IDENTICAL
)
{
return
;
}
$left_node
=
$cond_node
->
children
[
'left'
];
$right_node
=
$cond_node
->
children
[
'right'
];
if
(
self
::
isNullConstantNode
(
$left_node
))
{
if
(
ASTHasher
::
hash
(
$right_node
)
===
$true_node_hash
)
{
$this
->
warnDuplicateConditionalNullCoalescing
(
'null !== X ? X : Y'
,
$right_node
);
}
}
elseif
(
self
::
isNullConstantNode
(
$right_node
))
{
if
(
ASTHasher
::
hash
(
$left_node
)
===
$true_node_hash
)
{
$this
->
warnDuplicateConditionalNullCoalescing
(
'X !== null ? X : Y'
,
$left_node
);
}
}
}
/**
* @param int|string $true_node_hash
*/
private
function
checkUnaryOpOfConditional
(
Node
$cond_node
,
$true_node_hash
):
void
{
if
(
$cond_node
->
flags
!==
ast\flags\UNARY_BOOL_NOT
)
{
return
;
}
$expr
=
$cond_node
->
children
[
'expr'
];
if
(!
$expr
instanceof
Node
)
{
return
;
}
if
(
$expr
->
kind
===
ast\AST_CALL
)
{
$function
=
$expr
->
children
[
'expr'
];
if
(!
$function
instanceof
Node
||
$function
->
kind
!==
ast\AST_NAME
||
strcasecmp
((
string
)(
$function
->
children
[
'name'
]
??
''
),
'is_null'
)
!==
0
)
{
return
;
}
$args
=
$expr
->
children
[
'args'
]->
children
;
if
(
count
(
$args
)
!==
1
)
{
return
;
}
if
(
ASTHasher
::
hash
(
$args
[
0
])
===
$true_node_hash
)
{
$this
->
warnDuplicateConditionalNullCoalescing
(
'!is_null(X) ? X : Y'
,
$args
[
0
]);
}
}
}
/**
* @param Node|mixed $node
*/
private
static
function
isNullConstantNode
(
$node
):
bool
{
if
(!
$node
instanceof
Node
)
{
return
false
;
}
return
$node
->
kind
===
ast\AST_CONST
&&
strcasecmp
((
string
)(
$node
->
children
[
'name'
]->
children
[
'name'
]
??
''
),
'null'
)
===
0
;
}
/**
* @param ?(Node|string|int|float) $x_node
*/
private
function
warnDuplicateConditionalNullCoalescing
(
string
$expr
,
$x_node
):
void
{
$this
->
emitPluginIssue
(
$this
->
code_base
,
$this
->
context
,
'PhanPluginDuplicateConditionalNullCoalescing'
,
'"'
.
$expr
.
'" can usually be simplified to "X ?? Y" in PHP 7. The duplicated expression X was {CODE}'
,
[
ASTReverter
::
toShortString
(
$x_node
)]
);
}
}
/**
* This visitor analyzes node kinds that can be the root of expressions
* containing duplicate expressions, and is called on nodes in pre-order.
*/
class
RedundantNodePreAnalysisVisitor
extends
PluginAwarePreAnalysisVisitor
{
/**
* @override
*/
public
function
visitIf
(
Node
$node
):
void
{
if
(
count
(
$node
->
children
)
<=
1
)
{
// There can't be any duplicates.
return
;
}
// @phan-suppress-next-line PhanUndeclaredProperty
if
(
isset
(
$node
->
is_inside_else
))
{
return
;
}
$children
=
self
::
extractIfElseifChain
(
$node
);
// The checks of visitIf are done in pre-order (parent nodes analyzed before child nodes)
// so that checked_duplicate_if can be set, to avoid redundant work.
// @phan-suppress-next-line PhanUndeclaredProperty
if
(
isset
(
$node
->
checked_duplicate_if
))
{
return
;
}
// @phan-suppress-next-line PhanUndeclaredProperty
$node
->
checked_duplicate_if
=
true
;
[
'cond'
=>
$prev_cond
/*, 'stmts' => $prev_stmts */
]
=
$children
[
0
]->
children
;
// $prev_stmts_hash = ASTHasher::hash($prev_cond);
$condition_set
=
[
ASTHasher
::
hash
(
$prev_cond
)
=>
true
];
$N
=
count
(
$children
);
for
(
$i
=
1
;
$i
<
$N
;
$i
++)
{
[
'cond'
=>
$cond
/*, 'stmts' => $stmts */
]
=
$children
[
$i
]->
children
;
$cond_hash
=
ASTHasher
::
hash
(
$cond
);
if
(
isset
(
$condition_set
[
$cond_hash
]))
{
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$cond
->
lineno
??
$children
[
$i
]->
lineno
),
'PhanPluginDuplicateIfCondition'
,
'Saw the same condition {CODE} in an earlier if/elseif statement'
,
[
ASTReverter
::
toShortString
(
$cond
)]
);
}
else
{
$condition_set
[
$cond_hash
]
=
true
;
}
}
if
(!
isset
(
$cond
))
{
$stmts
=
$children
[
$N
-
1
]->
children
[
'stmts'
];
if
((
$stmts
->
children
??
null
)
&&
ASTHasher
::
hash
(
$stmts
)
===
ASTHasher
::
hash
(
$children
[
$N
-
2
]->
children
[
'stmts'
]))
{
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$children
[
$N
-
1
]->
lineno
),
'PhanPluginDuplicateIfStatements'
,
'The statements of the else duplicate the statements of the previous if/elseif statement with condition {CODE}'
,
[
ASTReverter
::
toShortString
(
$children
[
$N
-
2
]->
children
[
'cond'
])]
);
}
}
}
/**
* Visit a node of kind ast\AST_TRY, to check for adjacent catch blocks
*
* @override
* @suppress PhanPossiblyUndeclaredProperty
*/
public
function
visitTry
(
Node
$node
):
void
{
if
(
Config
::
get_closest_target_php_version_id
()
<
70100
)
{
return
;
}
$catches
=
$node
->
children
[
'catches'
]->
children
??
[];
$n
=
count
(
$catches
);
if
(
$n
<=
1
)
{
// There can't be any duplicates.
return
;
}
$prev_hash
=
ASTHasher
::
hash
(
$catches
[
0
]->
children
[
'stmts'
])
.
ASTHasher
::
hash
(
$catches
[
0
]->
children
[
'var'
]);
for
(
$i
=
1
;
$i
<
$n
;
$prev_hash
=
$cur_hash
,
$i
++)
{
$cur_hash
=
ASTHasher
::
hash
(
$catches
[
$i
]->
children
[
'stmts'
])
.
ASTHasher
::
hash
(
$catches
[
$i
]->
children
[
'var'
]);
if
(
$prev_hash
===
$cur_hash
)
{
$this
->
emitPluginIssue
(
$this
->
code_base
,
(
clone
$this
->
context
)->
withLineNumberStart
(
$catches
[
$i
]->
lineno
),
'PhanPluginDuplicateCatchStatementBody'
,
'The implementation of catch({CODE}) and catch({CODE}) are identical, and can be combined if the application only needs to supports php 7.1 and newer'
,
[
ASTReverter
::
toShortString
(
$catches
[
$i
-
1
]->
children
[
'class'
]),
ASTReverter
::
toShortString
(
$catches
[
$i
]->
children
[
'class'
]),
]
);
}
}
}
/**
* @param Node $node a node of kind ast\AST_IF
* @return list<Node> the list of AST_IF_ELEM nodes making up the chain of if/elseif/else if conditions.
* @suppress PhanPartialTypeMismatchReturn
*/
private
static
function
extractIfElseifChain
(
Node
$node
):
array
{
$children
=
$node
->
children
;
if
(
count
(
$children
)
<=
1
)
{
return
$children
;
}
$last_child
=
\end
(
$children
);
// Loop over the `} else {` blocks.
// @phan-suppress-next-line PhanPossiblyUndeclaredProperty
while
(
$last_child
->
children
[
'cond'
]
===
null
)
{
$first_stmt
=
$last_child
->
children
[
'stmts'
]->
children
[
0
]
??
null
;
if
(!(
$first_stmt
instanceof
Node
))
{
break
;
}
if
(
$first_stmt
->
kind
!==
ast\AST_IF
)
{
break
;
}
// @phan-suppress-next-line PhanUndeclaredProperty
$first_stmt
->
is_inside_else
=
true
;
\array_pop
(
$children
);
$if_elems
=
$first_stmt
->
children
;
foreach
(
$if_elems
as
$elem
)
{
$children
[]
=
$elem
;
}
$last_child
=
\end
(
$children
);
}
return
$children
;
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return
new
DuplicateExpressionPlugin
();
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, Oct 3, 13:47 (1 d, 2 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
fd/e4/3daa725a252af96407f56fb60425
Default Alt Text
DuplicateExpressionPlugin.php (20 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment