Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1431179
InferPureVisitor.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
17 KB
Referenced Files
None
Subscribers
None
InferPureVisitor.php
View Options
<?php
declare
(
strict_types
=
1
);
namespace
Phan\AST
;
use
ast
;
use
ast\Node
;
use
Exception
;
use
Phan\CodeBase
;
use
Phan\Exception\CodeBaseException
;
use
Phan\Exception\NodeException
;
use
Phan\Language\Context
;
use
Phan\Language\Element\Clazz
;
use
Phan\Language\Element\FunctionInterface
;
use
Phan\Language\FQSEN\FullyQualifiedClassName
;
use
Phan\Language\FQSEN\FullyQualifiedFunctionName
;
use
Phan\Plugin\Internal\UseReturnValuePlugin
;
use
Phan\Plugin\Internal\UseReturnValuePlugin\PureMethodGraph
;
use
Phan\Plugin\Internal\UseReturnValuePlugin\UseReturnValueVisitor
;
use
function
is_string
;
/**
* Used to check if a method is pure.
* Throws NodeException if it sees a node that isn't likely to be in a method that is free of side effects.
*
* This ignores many edge cases, including:
* - Magic properties
* - The possibility of emitting notices or throwing
* - Whether or not referenced elements exist (Phan checks that elsewhere)
*
* @phan-file-suppress PhanThrowTypeAbsent
*/
class
InferPureVisitor
extends
AnalysisVisitor
{
use
InferPureVisitorTrait
{
throwNodeException
as
visit
;
maybeInvokeAllChildNodes
as
visitClassConst
;
maybeInvokeAllChildNodes
as
visitStatic
;
maybeInvokeAllChildNodes
as
visitArray
;
maybeInvokeAllChildNodes
as
visitArrayElem
;
maybeInvokeAllChildNodes
as
visitEncapsList
;
maybeInvokeAllChildNodes
as
visitInstanceof
;
maybeInvokeAllChildNodes
as
visitBinaryOp
;
maybeInvokeAllChildNodes
as
visitUnaryOp
;
maybeInvokeAllChildNodes
as
visitDim
;
maybeInvokeAllChildNodes
as
visitCast
;
maybeInvokeAllChildNodes
as
visitConditional
;
maybeInvokeAllChildNodes
as
visitWhile
;
maybeInvokeAllChildNodes
as
visitDoWhile
;
maybeInvokeAllChildNodes
as
visitFor
;
maybeInvokeAllChildNodes
as
visitForeach
;
maybeInvokeAllChildNodes
as
visitIf
;
maybeInvokeAllChildNodes
as
visitIfElem
;
maybeInvokeAllChildNodes
as
visitSwitch
;
maybeInvokeAllChildNodes
as
visitSwitchList
;
maybeInvokeAllChildNodes
as
visitSwitchCase
;
maybeInvokeAllChildNodes
as
visitMatch
;
maybeInvokeAllChildNodes
as
visitMatchArm
;
maybeInvokeAllChildNodes
as
visitMatchArmList
;
maybeInvokeAllChildNodes
as
visitExprList
;
maybeInvokeAllChildNodes
as
visitYield
;
maybeInvokeAllChildNodes
as
visitYieldFrom
;
maybeInvokeAllChildNodes
as
visitReturn
;
maybeInvokeAllChildNodes
as
visitArgList
;
}
/** @var string the function fqsen being visited */
protected
$function_fqsen_label
;
/**
* Map from labels to functions which this had called, but were not certain to have been pure.
*
* @var array<string, FunctionInterface>
*/
protected
$unresolved_status_dependencies
=
[];
/**
* A graph that tracks information about whether functions are pure, and how they depend on other functions.
*
* @var ?PureMethodGraph
*/
protected
$pure_method_graph
;
public
function
__construct
(
CodeBase
$code_base
,
Context
$context
,
string
$function_fqsen_label
,
?
PureMethodGraph
$graph
=
null
)
{
$this
->
code_base
=
$code_base
;
$this
->
context
=
$context
;
$this
->
function_fqsen_label
=
$function_fqsen_label
;
$this
->
pure_method_graph
=
$graph
;
}
/**
* Generate a visitor from a function or method.
* This will be used for checking if the method is pure.
*/
public
static
function
fromFunction
(
CodeBase
$code_base
,
FunctionInterface
$func
,
?
PureMethodGraph
$graph
):
InferPureVisitor
{
return
new
self
(
$code_base
,
$func
->
getContext
(),
self
::
getLabelForFunction
(
$func
),
$graph
);
}
/**
* Returns the label UseReturnValuePlugin will use to look up whether this functions/methods is pure.
*/
public
function
getLabel
():
string
{
return
$this
->
function_fqsen_label
;
}
/**
* Returns an array of functions/methods with unknown pure status.
* If any of those functions are impure, then this function is impure.
*
* @return array<string, FunctionInterface>
*/
public
function
getUnresolvedStatusDependencies
():
array
{
return
$this
->
unresolved_status_dependencies
;
}
/**
* Returns the label UseReturnValuePlugin will use to look up whether functions/methods are pure.
*/
public
static
function
getLabelForFunction
(
FunctionInterface
$func
):
string
{
return
\strtolower
(
\ltrim
(
$func
->
getFQSEN
()->
__toString
(),
'
\\
'
));
}
// visitAssignRef
// visitThrow
// visitEcho
// visitPrint
// visitIncludeOrExec
public
function
visitVar
(
Node
$node
):
void
{
if
(!
\is_scalar
(
$node
->
children
[
'name'
]))
{
throw
new
NodeException
(
$node
);
}
}
/**
* @unused-param $node
* @override
*/
public
function
visitClassName
(
Node
$node
):
void
{
}
/**
* @unused-param $node
* @override
*/
public
function
visitMagicConst
(
Node
$node
):
void
{
}
/**
* @unused-param $node
* @override
*/
public
function
visitConst
(
Node
$node
):
void
{
}
/**
* @unused-param $node
* @override
*/
public
function
visitEmpty
(
Node
$node
):
void
{
$this
->
maybeInvoke
(
$node
->
children
[
'expr'
]);
}
/** @override */
public
function
visitIsset
(
Node
$node
):
void
{
$this
->
maybeInvoke
(
$node
->
children
[
'var'
]);
}
/**
* @unused-param $node
* @override
*/
public
function
visitContinue
(
Node
$node
):
void
{
}
/**
* @unused-param $node
* @override
*/
public
function
visitBreak
(
Node
$node
):
void
{
}
public
function
visitPreInc
(
Node
$node
):
void
{
$this
->
checkPureIncDec
(
$node
);
}
public
function
visitPreDec
(
Node
$node
):
void
{
$this
->
checkPureIncDec
(
$node
);
}
public
function
visitPostInc
(
Node
$node
):
void
{
$this
->
checkPureIncDec
(
$node
);
}
public
function
visitPostDec
(
Node
$node
):
void
{
$this
->
checkPureIncDec
(
$node
);
}
protected
function
checkPureIncDec
(
Node
$node
):
void
{
$var
=
$node
->
children
[
'var'
];
if
(!
$var
instanceof
Node
)
{
throw
new
NodeException
(
$node
);
}
if
(
$var
->
kind
!==
ast\AST_VAR
)
{
throw
new
NodeException
(
$var
);
}
$this
->
visitVar
(
$var
);
}
/**
* @param Node|string|int|float|null $node
*/
final
protected
function
maybeInvoke
(
$node
):
void
{
if
(
$node
instanceof
Node
)
{
$this
->
__invoke
(
$node
);
}
}
public
function
visitNullsafeProp
(
Node
$node
):
void
{
$this
->
visitProp
(
$node
);
}
public
function
visitProp
(
Node
$node
):
void
{
[
'expr'
=>
$expr
,
'prop'
=>
$prop
]
=
$node
->
children
;
if
(!
$expr
instanceof
Node
)
{
throw
new
NodeException
(
$node
);
}
$this
->
__invoke
(
$expr
);
if
(
$prop
instanceof
Node
)
{
throw
new
NodeException
(
$prop
);
}
}
/** @override */
public
function
visitStmtList
(
Node
$node
):
void
{
foreach
(
$node
->
children
as
$stmt
)
{
if
(
$stmt
instanceof
Node
)
{
$this
->
__invoke
(
$stmt
);
}
}
}
/** @override */
public
function
visitStaticProp
(
Node
$node
):
void
{
[
'class'
=>
$class
,
'prop'
=>
$prop
]
=
$node
->
children
;
if
(!
$class
instanceof
Node
)
{
throw
new
NodeException
(
$node
);
}
$this
->
__invoke
(
$class
);
if
(
$prop
instanceof
Node
)
{
throw
new
NodeException
(
$prop
);
}
}
/** @override */
public
function
visitCast
(
Node
$node
):
void
{
$this
->
maybeInvoke
(
$node
->
children
[
'expr'
]);
}
/**
* @unused-param $node
* @override
*/
public
function
visitGoto
(
Node
$node
):
void
{
}
/**
* @unused-param $node
* @override
*/
public
function
visitLabel
(
Node
$node
):
void
{
}
/** @override */
public
function
visitAssignOp
(
Node
$node
):
void
{
$this
->
visitAssign
(
$node
);
}
/** @override */
public
function
visitAssign
(
Node
$node
):
void
{
[
'var'
=>
$var
,
'expr'
=>
$expr
]
=
$node
->
children
;
if
(!
$var
instanceof
Node
)
{
throw
new
NodeException
(
$node
);
}
$this
->
checkVarKindOfAssign
(
$var
);
$this
->
__invoke
(
$var
);
if
(
$expr
instanceof
Node
)
{
$this
->
__invoke
(
$expr
);
}
}
private
function
checkVarKindOfAssign
(
Node
$var
):
void
{
if
(
$var
->
kind
===
ast\AST_VAR
)
{
return
;
}
elseif
(
$var
->
kind
===
ast\AST_PROP
)
{
// Functions that assign to properties aren't pure,
// unless assigning to $this->prop in a constructor.
if
(
\preg_match
(
'/::__construct$/iD'
,
$this
->
function_fqsen_label
))
{
$name
=
$var
->
children
[
'expr'
];
if
(
$name
instanceof
Node
&&
$name
->
kind
===
ast\AST_VAR
&&
$name
->
children
[
'name'
]
===
'this'
)
{
return
;
}
}
}
throw
new
NodeException
(
$var
);
}
public
function
visitNew
(
Node
$node
):
void
{
$name_node
=
$node
->
children
[
'class'
];
if
(!(
$name_node
instanceof
Node
&&
$name_node
->
kind
===
ast\AST_NAME
))
{
throw
new
NodeException
(
$node
);
}
// "Fatal error: Cannot create Closure for new expression" (for AST_CALLABLE_CONVERT) is caught elsewhere
$this
->
__invoke
(
$node
->
children
[
'args'
]);
try
{
$class_list
=
(
new
ContextNode
(
$this
->
code_base
,
$this
->
context
,
$name_node
))->
getClassList
(
false
,
ContextNode
::
CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME
);
}
catch
(
Exception
$_
)
{
throw
new
NodeException
(
$name_node
);
}
if
(!
$class_list
)
{
throw
new
NodeException
(
$name_node
,
'no class found'
);
}
foreach
(
$class_list
as
$class
)
{
if
(
$class
->
isPHPInternal
())
{
// TODO build a list of internal classes where result of new() is often unused.
continue
;
}
if
(!
$class
->
hasMethodWithName
(
$this
->
code_base
,
'__construct'
,
true
))
{
continue
;
}
$this
->
checkCalledFunction
(
$node
,
$class
->
getMethodByName
(
$this
->
code_base
,
'__construct'
));
}
}
/**
* @unused-param $node
* @override
*/
public
function
visitName
(
Node
$node
):
void
{
// do nothing
}
/** @override */
public
function
visitCall
(
Node
$node
):
void
{
$expr
=
$node
->
children
[
'expr'
];
if
(!
$expr
instanceof
Node
)
{
throw
new
NodeException
(
$node
);
}
if
(
$expr
->
kind
!==
ast\AST_NAME
)
{
// XXX this is deliberately a limited subset of what full analysis would do,
// so this can't infer locally set closures, etc.
throw
new
NodeException
(
$expr
);
}
$found_function
=
false
;
try
{
$function_list_generator
=
(
new
ContextNode
(
$this
->
code_base
,
$this
->
context
,
$expr
))->
getFunctionFromNode
(
true
);
foreach
(
$function_list_generator
as
$function
)
{
$this
->
checkCalledFunction
(
$node
,
$function
);
$found_function
=
true
;
}
}
catch
(
CodeBaseException
$_
)
{
// ignore it.
}
if
(!
$found_function
)
{
throw
new
NodeException
(
$expr
,
'not a function'
);
}
$this
->
__invoke
(
$node
->
children
[
'args'
]);
}
public
function
visitStaticCall
(
Node
$node
):
void
{
$method
=
$node
->
children
[
'method'
];
if
(!
\is_string
(
$method
))
{
throw
new
NodeException
(
$node
);
}
$class
=
$node
->
children
[
'class'
];
if
(!(
$class
instanceof
Node
))
{
throw
new
NodeException
(
$node
);
}
if
(
$class
->
kind
!==
ast\AST_NAME
)
{
throw
new
NodeException
(
$class
,
'not a name'
);
}
try
{
$union_type
=
UnionTypeVisitor
::
unionTypeFromClassNode
(
$this
->
code_base
,
$this
->
context
,
$class
);
}
catch
(
Exception
$_
)
{
throw
new
NodeException
(
$class
,
'could not get type'
);
}
// TODO: Check all classes in union and intersection types instead up to a limit?
if
(
$union_type
->
typeCount
()
!==
1
)
{
throw
new
NodeException
(
$class
);
}
$type
=
$union_type
->
getTypeSet
()[
0
];
if
(!
$type
->
isObjectWithKnownFQSEN
())
{
throw
new
NodeException
(
$class
);
}
$class_fqsen
=
$type
->
asFQSEN
();
if
(!(
$class_fqsen
instanceof
FullyQualifiedClassName
))
{
throw
new
NodeException
(
$class
);
}
if
(!
$this
->
code_base
->
hasClassWithFQSEN
(
$class_fqsen
))
{
throw
new
NodeException
(
$class
);
}
try
{
$class
=
$this
->
code_base
->
getClassByFQSEN
(
$class_fqsen
);
}
catch
(
Exception
$_
)
{
throw
new
NodeException
(
$node
);
}
if
(!
$class
->
hasMethodWithName
(
$this
->
code_base
,
$method
,
true
))
{
throw
new
NodeException
(
$node
,
'no method'
);
}
$this
->
checkCalledFunction
(
$node
,
$class
->
getMethodByName
(
$this
->
code_base
,
$method
));
$this
->
__invoke
(
$node
->
children
[
'args'
]);
}
public
function
visitNullsafeMethodCall
(
Node
$node
):
void
{
$this
->
visitMethodCall
(
$node
);
}
public
function
visitMethodCall
(
Node
$node
):
void
{
$method_name
=
$node
->
children
[
'method'
];
if
(!
\is_string
(
$method_name
))
{
throw
new
NodeException
(
$node
);
}
$expr
=
$node
->
children
[
'expr'
];
if
(!
$expr
instanceof
Node
)
{
throw
new
NodeException
(
$node
);
}
$class
=
$this
->
getClassForVariable
(
$expr
);
if
(!
$class
->
hasMethodWithName
(
$this
->
code_base
,
$method_name
,
true
))
{
throw
new
NodeException
(
$expr
,
'does not have method'
);
}
$this
->
checkCalledFunction
(
$node
,
$class
->
getMethodByName
(
$this
->
code_base
,
$method_name
));
$this
->
__invoke
(
$node
->
children
[
'args'
]);
}
protected
function
getClassForVariable
(
Node
$expr
):
Clazz
{
if
(!
$this
->
context
->
isInClassScope
())
{
// We don't track variables in UseReturnValuePlugin
throw
new
NodeException
(
$expr
,
'method call seen outside class scope'
);
}
if
(
$expr
->
kind
!==
ast\AST_VAR
)
{
throw
new
NodeException
(
$expr
,
'expected simple variable'
);
}
$var_name
=
$expr
->
children
[
'name'
];
if
(!
is_string
(
$var_name
))
{
// TODO: Support static properties, (new X()), other expressions with inferable types
throw
new
NodeException
(
$expr
,
'variable name is not a string'
);
}
if
(
$var_name
!==
'this'
)
{
throw
new
NodeException
(
$expr
,
'not $this'
);
}
if
(!
$this
->
context
->
isInClassScope
())
{
throw
new
NodeException
(
$expr
,
'Not in class scope'
);
}
return
$this
->
context
->
getClassInScope
(
$this
->
code_base
);
}
/**
* @param Node $node the node of the call, with 'args'
*/
protected
function
checkCalledFunction
(
Node
$node
,
FunctionInterface
$method
):
void
{
if
(
$method
->
isPure
())
{
return
;
}
$label
=
self
::
getLabelForFunction
(
$method
);
$value
=
(
UseReturnValuePlugin
::
HARDCODED_FQSENS
[
$label
]
??
false
);
if
(
$value
===
true
)
{
return
;
}
elseif
(
$value
===
UseReturnValuePlugin
::
SPECIAL_CASE
)
{
if
(
UseReturnValueVisitor
::
doesSpecialCaseHaveSideEffects
(
$label
,
$node
))
{
// infer that var_export($x, true) is pure but not var_export($x)
throw
new
NodeException
(
$node
,
$label
);
}
return
;
}
if
(
$method
->
isPHPInternal
())
{
// Something such as printf that isn't pure. Or something that isn't in the HARDCODED_FQSENS.
throw
new
NodeException
(
$node
,
'internal method is not pure'
);
}
if
(
$label
===
$this
->
function_fqsen_label
)
{
return
;
}
if
(
$this
->
pure_method_graph
)
{
$this
->
unresolved_status_dependencies
[
$label
]
=
$method
;
return
;
}
throw
new
NodeException
(
$node
,
$label
);
}
public
function
visitClosure
(
Node
$node
):
void
{
$closure_fqsen
=
FullyQualifiedFunctionName
::
fromClosureInContext
(
(
clone
$this
->
context
)->
withLineNumberStart
(
$node
->
lineno
),
$node
);
if
(!
$this
->
code_base
->
hasFunctionWithFQSEN
(
$closure_fqsen
))
{
throw
new
NodeException
(
$node
,
"Failed lookup of closure_fqsen"
);
}
$this
->
checkCalledFunction
(
$node
,
$this
->
code_base
->
getFunctionByFQSEN
(
$closure_fqsen
));
}
public
function
visitArrowFunc
(
Node
$node
):
void
{
$this
->
visitClosure
(
$node
);
}
/**
* @unused-param $node
* @override
*/
public
function
visitCallableConvert
(
Node
$node
):
void
{
}
public
function
visitNamedArg
(
Node
$node
):
void
{
$expr
=
$node
->
children
[
'expr'
];
if
(
$expr
instanceof
Node
)
{
$this
->
__invoke
(
$expr
);
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 20:10 (2 h, 57 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
1c/c8/e5af964d5b0230c7f644e6d6f797
Default Alt Text
InferPureVisitor.php (17 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment