Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4091699
UnusedSuppressionPlugin.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
10 KB
Referenced Files
None
Subscribers
None
UnusedSuppressionPlugin.php
View Options
<?php
declare
(
strict_types
=
1
);
use
ast\Node
;
use
Phan\CodeBase
;
use
Phan\Config
;
use
Phan\Language\Context
;
use
Phan\Language\Element\AddressableElement
;
use
Phan\Language\Element\Clazz
;
use
Phan\Language\Element\Func
;
use
Phan\Language\Element\Method
;
use
Phan\Language\Element\Property
;
use
Phan\Plugin\ConfigPluginSet
;
use
Phan\PluginV3
;
use
Phan\PluginV3\AnalyzeClassCapability
;
use
Phan\PluginV3\AnalyzeFunctionCapability
;
use
Phan\PluginV3\AnalyzeMethodCapability
;
use
Phan\PluginV3\AnalyzePropertyCapability
;
use
Phan\PluginV3\BeforeAnalyzeFileCapability
;
use
Phan\PluginV3\FinalizeProcessCapability
;
use
Phan\PluginV3\SuppressionCapability
;
/**
* Check for unused (at)suppress annotations.
*
* NOTE! This plugin only produces correct results when Phan
* is run on a single processor (via the `-j1` flag).
*/
class
UnusedSuppressionPlugin
extends
PluginV3
implements
BeforeAnalyzeFileCapability
,
AnalyzeClassCapability
,
AnalyzeFunctionCapability
,
AnalyzeMethodCapability
,
AnalyzePropertyCapability
,
FinalizeProcessCapability
{
/**
* @var AddressableElement[] - Analysis is postponed until finalizeProcess.
* Issues may have been emitted after `$this->analyze*()` were called,
* which is why those methods postpone the check until analysis is finished.
*
* Also, looping over all elements again would be slow.
*
* These are currently unique, even when quick_mode is false.
*/
private
$elements_for_postponed_analysis
=
[];
/**
* @var string[] a list of files where checks for unused suppressions was postponed
* (Because of non-quick mode, we may emit issues in a file after analysis has run on that file)
*/
private
$files_for_postponed_analysis
=
[];
/**
* @var array<string,array<string,array<string,array<int,int>>>> stores the suppressions for active plugins
* maps plugin class to
* file name to
* issue type to
* unique list of line numbers of suppressions
*/
private
$plugin_active_suppression_list
=
[];
/**
* @param CodeBase $code_base
* The code base in which the element exists
*
* @param AddressableElement $element
* Any element such as function, method, class
* (which has an FQSEN)
*/
private
static
function
analyzeAddressableElement
(
CodeBase
$code_base
,
AddressableElement
$element
):
void
{
// Get the set of suppressed issues on the element
$suppress_issue_list
=
$element
->
getSuppressIssueList
();
if
(
\array_key_exists
(
'UnusedSuppression'
,
$suppress_issue_list
))
{
// The element's doc comment is suppressing everything emitted by this plugin.
return
;
}
// Check to see if any are unused
foreach
(
$suppress_issue_list
as
$issue_type
=>
$use_count
)
{
if
(
0
!==
$use_count
)
{
continue
;
}
if
(
in_array
(
$issue_type
,
self
::
getUnusedSuppressionIgnoreList
(),
true
))
{
continue
;
}
self
::
emitIssue
(
$code_base
,
$element
->
getContext
(),
'UnusedSuppression'
,
"Element {FUNCTIONLIKE} suppresses issue {ISSUETYPE} but does not use it"
,
[(
string
)
$element
->
getFQSEN
(),
$issue_type
]
);
}
}
private
function
postponeAnalysisOfElement
(
AddressableElement
$element
):
void
{
if
(
count
(
$element
->
getSuppressIssueList
())
===
0
)
{
// There are no suppressions, so there's no reason to check this
return
;
}
$this
->
elements_for_postponed_analysis
[]
=
$element
;
}
/**
* @param CodeBase $code_base @unused-param
* The code base in which the class exists
*
* @param Clazz $class
* A class being analyzed
* @override
*/
public
function
analyzeClass
(
CodeBase
$code_base
,
Clazz
$class
):
void
{
$this
->
postponeAnalysisOfElement
(
$class
);
}
/**
* @param CodeBase $code_base @unused-param
* The code base in which the method exists
*
* @param Method $method
* A method being analyzed
* @override
*/
public
function
analyzeMethod
(
CodeBase
$code_base
,
Method
$method
):
void
{
// Ignore methods inherited by subclasses
if
(
$method
->
getFQSEN
()
!==
$method
->
getRealDefiningFQSEN
())
{
return
;
}
$this
->
postponeAnalysisOfElement
(
$method
);
}
/**
* @param CodeBase $code_base @unused-param
* The code base in which the function exists
*
* @param Func $function
* A function being analyzed
* @override
*/
public
function
analyzeFunction
(
CodeBase
$code_base
,
Func
$function
):
void
{
$this
->
postponeAnalysisOfElement
(
$function
);
}
/**
* @param CodeBase $code_base @unused-param
* The code base in which the property exists
*
* @param Property $property
* A property being analyzed
* @override
*/
public
function
analyzeProperty
(
CodeBase
$code_base
,
Property
$property
):
void
{
if
(
$property
->
getFQSEN
()
!==
$property
->
getRealDefiningFQSEN
())
{
return
;
}
$this
->
elements_for_postponed_analysis
[]
=
$property
;
}
/**
* NOTE! This plugin only produces correct results when Phan
* is run on a single processor (via the `-j1` flag).
* Putting this hook in finalizeProcess() just minimizes the incorrect result counts.
* @override
*/
public
function
finalizeProcess
(
CodeBase
$code_base
):
void
{
foreach
(
$this
->
elements_for_postponed_analysis
as
$element
)
{
self
::
analyzeAddressableElement
(
$code_base
,
$element
);
}
$this
->
analyzePluginSuppressions
(
$code_base
);
}
private
function
analyzePluginSuppressions
(
CodeBase
$code_base
):
void
{
$suppression_plugin_set
=
ConfigPluginSet
::
instance
()->
getSuppressionPluginSet
();
if
(
count
(
$suppression_plugin_set
)
===
0
)
{
return
;
}
foreach
(
$this
->
files_for_postponed_analysis
as
$file_path
)
{
foreach
(
$suppression_plugin_set
as
$plugin
)
{
$this
->
analyzePluginSuppressionsForFile
(
$code_base
,
$plugin
,
$file_path
);
}
}
}
/**
* @return list<string>
*/
private
static
function
getUnusedSuppressionIgnoreList
():
array
{
return
Config
::
getValue
(
'plugin_config'
)[
'unused_suppression_ignore_list'
]
??
[];
}
private
static
function
getReportOnlyWhitelisted
():
bool
{
return
Config
::
getValue
(
'plugin_config'
)[
'unused_suppression_whitelisted_only'
]
??
false
;
}
private
static
function
shouldReportUnusedSuppression
(
string
$issue_type
):
bool
{
$ignore_list
=
self
::
getUnusedSuppressionIgnoreList
();
$only_whitelisted
=
self
::
getReportOnlyWhitelisted
();
$issue_whitelist
=
Config
::
getValue
(
'whitelist_issue_types'
)
??
[];
return
!
in_array
(
$issue_type
,
$ignore_list
,
true
)
&&
(!
$only_whitelisted
||
in_array
(
$issue_type
,
$issue_whitelist
,
true
));
}
private
function
analyzePluginSuppressionsForFile
(
CodeBase
$code_base
,
SuppressionCapability
$plugin
,
string
$relative_file_path
):
void
{
$absolute_file_path
=
Config
::
projectPath
(
$relative_file_path
);
$plugin_class
=
\get_class
(
$plugin
);
$name_pos
=
\strrpos
(
$plugin_class
,
'
\\
'
);
if
(
$name_pos
!==
false
)
{
$plugin_name
=
\substr
(
$plugin_class
,
$name_pos
+
1
);
}
else
{
$plugin_name
=
$plugin_class
;
}
$plugin_suppressions
=
$plugin
->
getIssueSuppressionList
(
$code_base
,
$absolute_file_path
);
$plugin_successful_suppressions
=
$this
->
plugin_active_suppression_list
[
$plugin_class
][
$absolute_file_path
]
??
null
;
foreach
(
$plugin_suppressions
as
$issue_type
=>
$line_list
)
{
foreach
(
$line_list
as
$lineno
=>
$lineno_of_comment
)
{
if
(
isset
(
$plugin_successful_suppressions
[
$issue_type
][
$lineno
]))
{
continue
;
}
// TODO: finish letting plugins suppress UnusedSuppression on other plugins
$issue_kind
=
'UnusedPluginSuppression'
;
$message
=
'Plugin {STRING_LITERAL} suppresses issue {ISSUETYPE} on this line but this suppression is unused or suppressed elsewhere'
;
if
(
$lineno
===
0
)
{
$issue_kind
=
'UnusedPluginFileSuppression'
;
$message
=
'Plugin {STRING_LITERAL} suppresses issue {ISSUETYPE} in this file but this suppression is unused or suppressed elsewhere'
;
}
if
(
isset
(
$plugin_suppressions
[
'UnusedSuppression'
][
$lineno_of_comment
]))
{
continue
;
}
if
(
isset
(
$plugin_suppressions
[
$issue_kind
][
$lineno_of_comment
]))
{
continue
;
}
if
(!
self
::
shouldReportUnusedSuppression
(
$issue_type
))
{
continue
;
}
self
::
emitIssue
(
$code_base
,
(
new
Context
())->
withFile
(
$relative_file_path
)->
withLineNumberStart
(
$lineno_of_comment
),
$issue_kind
,
$message
,
[
$plugin_name
,
$issue_type
]
);
}
}
return
;
}
/**
* @unused-param $code_base
* @unused-param $file_contents
* @unused-param $node
*/
public
function
beforeAnalyzeFile
(
CodeBase
$code_base
,
Context
$context
,
string
$file_contents
,
Node
$node
):
void
{
$file
=
$context
->
getFile
();
$this
->
files_for_postponed_analysis
[
$file
]
=
$file
;
}
/**
* Record the fact that $plugin caused suppressions in $file_path for issue $issue_type due to an annotation around $line
* @internal
*/
public
function
recordPluginSuppression
(
SuppressionCapability
$plugin
,
string
$file_path
,
string
$issue_type
,
int
$line
):
void
{
$file_name
=
Config
::
projectPath
(
$file_path
);
$plugin_class
=
\get_class
(
$plugin
);
$this
->
plugin_active_suppression_list
[
$plugin_class
][
$file_name
][
$issue_type
][
$line
]
=
$line
;
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return
new
UnusedSuppressionPlugin
();
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Aug 18 2026, 11:51 (5 w, 10 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
f0/11/30c9fc99c4b3181b3e47497227cb
Default Alt Text
UnusedSuppressionPlugin.php (10 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment