Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4148358
HasPHPDocPlugin.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
14 KB
Referenced Files
None
Subscribers
None
HasPHPDocPlugin.php
View Options
<?php
declare
(
strict_types
=
1
);
namespace
HasPHPDocPlugin
;
use
AssertionError
;
use
ast
;
use
ast\Node
;
use
Phan\CodeBase
;
use
Phan\Config
;
use
Phan\Language\Element\ClassElement
;
use
Phan\Language\Element\Clazz
;
use
Phan\Language\Element\Comment
;
use
Phan\Language\Element\Func
;
use
Phan\Language\Element\MarkupDescription
;
use
Phan\Library\StringUtil
;
use
Phan\PluginV3
;
use
Phan\PluginV3\AnalyzeClassCapability
;
use
Phan\PluginV3\AnalyzeFunctionCapability
;
use
Phan\PluginV3\PluginAwarePostAnalysisVisitor
;
use
Phan\PluginV3\PostAnalyzeNodeCapability
;
use
function
array_shift
;
use
function
count
;
use
function
gettype
;
use
function
is_string
;
use
function
json_encode
;
use
function
ltrim
;
use
function
preg_match
;
use
function
strpos
;
use
function
ucfirst
;
use
const
JSON_UNESCAPED_SLASHES
;
use
const
JSON_UNESCAPED_UNICODE
;
/**
* This file checks if an element (class or property) has a PHPDoc comment,
* and that Phan can extract a plaintext summary/description from that comment.
*
* (e.g. for generating a hover description in the language server)
*
* It hooks into these events:
*
* - analyzeClass
* Once all classes are parsed, this method will be called
* on every method in the code base
*
* - analyzeProperty
* Once all properties have been parsed, this method will
* be called on every property in the code base.
* - analyzeMethod
* Once all methods have been parsed, this method will
* be called on every method in the code base.
* - analyzeFunction
* Once all functions have been parsed, this method will
* be called on every function/closure in the code base.
*
* 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
* @internal
*/
final
class
HasPHPDocPlugin
extends
PluginV3
implements
AnalyzeClassCapability
,
AnalyzeFunctionCapability
,
PostAnalyzeNodeCapability
{
/** @var ?string a regex to use to exclude methods from phpdoc checks. */
public
static
$method_filter
;
public
function
__construct
()
{
$plugin_config
=
Config
::
getValue
(
'plugin_config'
);
self
::
$method_filter
=
$plugin_config
[
'has_phpdoc_method_ignore_regex'
]
??
null
;
}
/**
* @param CodeBase $code_base
* 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
{
if
(
$class
->
isAnonymous
())
{
// Probably not useful in many cases to document a short anonymous class.
return
;
}
$doc_comment
=
$class
->
getDocComment
();
if
(!
StringUtil
::
isNonZeroLengthString
(
$doc_comment
))
{
self
::
emitIssue
(
$code_base
,
$class
->
getContext
(),
'PhanPluginNoCommentOnClass'
,
'Class {CLASS} has no doc comment'
,
[
$class
->
getFQSEN
()]
);
return
;
}
$description
=
MarkupDescription
::
extractDescriptionFromDocComment
(
$class
);
if
(!
StringUtil
::
isNonZeroLengthString
(
$description
))
{
if
(
strpos
(
$doc_comment
,
'@deprecated'
)
!==
false
)
{
return
;
}
self
::
emitIssue
(
$code_base
,
$class
->
getContext
(),
'PhanPluginDescriptionlessCommentOnClass'
,
'Class {CLASS} has no readable description: {STRING_LITERAL}'
,
[
$class
->
getFQSEN
(),
self
::
getDocCommentRepresentation
(
$doc_comment
)]
);
return
;
}
}
/**
* @param CodeBase $code_base
* 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
{
if
(
$function
->
isPHPInternal
())
{
// This isn't user-defined, there's no reason to warn or way to change it.
return
;
}
if
(
$function
->
isNSInternal
(
$code_base
))
{
// (at)internal are internal to the library, and there's less of a need to document them
return
;
}
if
(
$function
->
isClosure
())
{
// Probably not useful in many cases to document a short closure passed to array_map, etc.
return
;
}
$doc_comment
=
$function
->
getDocComment
();
if
(!
StringUtil
::
isNonZeroLengthString
(
$doc_comment
))
{
self
::
emitIssue
(
$code_base
,
$function
->
getContext
(),
"PhanPluginNoCommentOnFunction"
,
"Function {FUNCTION} has no doc comment"
,
[
$function
->
getFQSEN
()]
);
return
;
}
$description
=
MarkupDescription
::
extractDescriptionFromDocComment
(
$function
);
if
(!
StringUtil
::
isNonZeroLengthString
(
$description
))
{
self
::
emitIssue
(
$code_base
,
$function
->
getContext
(),
"PhanPluginDescriptionlessCommentOnFunction"
,
"Function {FUNCTION} has no readable description: {STRING_LITERAL}"
,
[
$function
->
getFQSEN
(),
self
::
getDocCommentRepresentation
(
$doc_comment
)]
);
return
;
}
}
/**
* Encode the doc comment in a one-line form that can be used in Phan's issue message.
* @internal
*/
public
static
function
getDocCommentRepresentation
(
string
$doc_comment
):
string
{
return
(
string
)
json_encode
(
MarkupDescription
::
getDocCommentWithoutWhitespace
(
$doc_comment
),
JSON_UNESCAPED_SLASHES
|
JSON_UNESCAPED_UNICODE
);
}
public
static
function
getPostAnalyzeNodeVisitorClassName
():
string
{
return
(
bool
)(
Config
::
getValue
(
'plugin_config'
)[
'has_phpdoc_check_duplicates'
]
??
false
)
?
DuplicatePHPDocCheckerPlugin
::
class
:
BasePHPDocCheckerPlugin
::
class
;
}
}
/** Infer property and class doc comments and warn */
class
BasePHPDocCheckerPlugin
extends
PluginAwarePostAnalysisVisitor
{
/** @return array{0:list<ClassElementEntry>,1:list<ClassElementEntry>} */
public
function
visitClass
(
Node
$node
):
array
{
$class
=
$this
->
context
->
getClassInScope
(
$this
->
code_base
);
$property_descriptions
=
[];
$method_descriptions
=
[];
foreach
(
$node
->
children
[
'stmts'
]->
children
??
[]
as
$element
)
{
if
(!(
$element
instanceof
Node
))
{
throw
new
AssertionError
(
"All properties of ast
\A
ST_CLASS's statement list must be nodes, saw "
.
gettype
(
$element
));
}
switch
(
$element
->
kind
)
{
case
ast\AST_METHOD
:
$entry
=
$this
->
checkMethodDescription
(
$class
,
$element
);
if
(
$entry
)
{
$method_descriptions
[]
=
$entry
;
}
break
;
case
ast\AST_PROP_GROUP
:
$entry
=
$this
->
checkPropGroupDescription
(
$class
,
$element
);
if
(
$entry
)
{
$property_descriptions
[]
=
$entry
;
}
break
;
}
}
return
[
$property_descriptions
,
$method_descriptions
];
}
/**
* @param Node $node a node of kind ast\AST_METHOD
*/
private
function
checkMethodDescription
(
Clazz
$class
,
Node
$node
):
?
ClassElementEntry
{
$method_name
=
(
string
)
$node
->
children
[
'name'
];
$method
=
$class
->
getMethodByName
(
$this
->
code_base
,
$method_name
);
if
(
$method
->
isMagic
())
{
// Ignore construct
return
null
;
}
if
(
$method
->
isOverride
())
{
return
null
;
}
$method_filter
=
HasPHPDocPlugin
::
$method_filter
;
if
(
is_string
(
$method_filter
))
{
$fqsen_string
=
ltrim
((
string
)
$method
->
getFQSEN
(),
'
\\
'
);
if
(
preg_match
(
$method_filter
,
$fqsen_string
)
>
0
)
{
return
null
;
}
}
$doc_comment
=
$method
->
getDocComment
();
if
(!
StringUtil
::
isNonZeroLengthString
(
$doc_comment
))
{
$visibility_upper
=
ucfirst
(
$method
->
getVisibilityName
());
self
::
emitPluginIssue
(
$this
->
code_base
,
$method
->
getContext
(),
"PhanPluginNoCommentOn{$visibility_upper}Method"
,
"$visibility_upper method {METHOD} has no doc comment"
,
[
$method
->
getFQSEN
()]
);
return
null
;
}
$description
=
MarkupDescription
::
extractDescriptionFromDocComment
(
$method
);
if
(!
StringUtil
::
isNonZeroLengthString
(
$description
))
{
$visibility_upper
=
ucfirst
(
$method
->
getVisibilityName
());
self
::
emitPluginIssue
(
$this
->
code_base
,
$method
->
getContext
(),
"PhanPluginDescriptionlessCommentOn{$visibility_upper}Method"
,
"$visibility_upper method {METHOD} has no readable description: {STRING_LITERAL}"
,
[
$method
->
getFQSEN
(),
HasPHPDocPlugin
::
getDocCommentRepresentation
(
$doc_comment
)]
);
return
null
;
}
return
new
ClassElementEntry
(
$method
,
\trim
(
\preg_replace
(
'/
\s
+/'
,
' '
,
$description
)));
}
/**
* @param Node $node a node of type ast\AST_PROP_GROUP
*/
private
function
checkPropGroupDescription
(
Clazz
$class
,
Node
$node
):
?
ClassElementEntry
{
$property_name
=
$node
->
children
[
'props'
]->
children
[
0
]->
children
[
'name'
]
??
null
;
if
(!
is_string
(
$property_name
))
{
return
null
;
}
$property
=
$class
->
getPropertyByName
(
$this
->
code_base
,
$property_name
);
$doc_comment
=
$property
->
getDocComment
();
if
(!
StringUtil
::
isNonZeroLengthString
(
$doc_comment
))
{
$visibility_upper
=
ucfirst
(
$property
->
getVisibilityName
());
self
::
emitPluginIssue
(
$this
->
code_base
,
$property
->
getContext
(),
"PhanPluginNoCommentOn{$visibility_upper}Property"
,
"$visibility_upper property {PROPERTY} has no doc comment"
,
[
$property
->
getRepresentationForIssue
()]
);
return
null
;
}
// @phan-suppress-next-line PhanAccessMethodInternal
$description
=
MarkupDescription
::
extractDocComment
(
$doc_comment
,
Comment
::
ON_PROPERTY
,
null
,
true
);
if
(!
StringUtil
::
isNonZeroLengthString
(
$description
))
{
$visibility_upper
=
ucfirst
(
$property
->
getVisibilityName
());
self
::
emitPluginIssue
(
$this
->
code_base
,
$property
->
getContext
(),
"PhanPluginDescriptionlessCommentOn{$visibility_upper}Property"
,
"$visibility_upper property {PROPERTY} has no readable description: {STRING_LITERAL}"
,
[
$property
->
getRepresentationForIssue
(),
HasPHPDocPlugin
::
getDocCommentRepresentation
(
$doc_comment
)]
);
return
null
;
}
return
new
ClassElementEntry
(
$property
,
\trim
(
\preg_replace
(
'/
\s
+/'
,
' '
,
$description
)));
}
}
/**
* Describes a property group or a method node and the associated description
* @phan-immutable
* @internal
*/
final
class
ClassElementEntry
{
/** @var ClassElement the element (or element group) */
public
$element
;
/** @var string the phpdoc description */
public
$description
;
public
function
__construct
(
ClassElement
$element
,
string
$description
)
{
$this
->
element
=
$element
;
$this
->
description
=
$description
;
}
}
/**
* Check if phpdoc of property groups and methods are duplicated
* @internal
*/
final
class
DuplicatePHPDocCheckerPlugin
extends
BasePHPDocCheckerPlugin
{
/** No-op */
public
function
visitClass
(
Node
$node
):
array
{
[
$property_descriptions
,
$method_descriptions
]
=
parent
::
visitClass
(
$node
);
foreach
(
self
::
findGroups
(
$property_descriptions
)
as
$entries
)
{
$first_entry
=
array_shift
(
$entries
);
if
(!
$first_entry
instanceof
ClassElementEntry
)
{
throw
new
AssertionError
(
'Expected $entries of $property_descriptions to be a group of 1 or more entries'
);
}
$first_property
=
$first_entry
->
element
;
foreach
(
$entries
as
$entry
)
{
$property
=
$entry
->
element
;
self
::
emitPluginIssue
(
$this
->
code_base
,
$property
->
getContext
(),
"PhanPluginDuplicatePropertyDescription"
,
"Property {PROPERTY} has the same description as the property
\$
{PROPERTY} on line {LINE}: {COMMENT}"
,
[
$property
->
getRepresentationForIssue
(),
$first_property
->
getName
(),
$first_property
->
getContext
()->
getLineNumberStart
(),
$first_entry
->
description
]
);
}
}
foreach
(
self
::
findGroups
(
$method_descriptions
)
as
$entries
)
{
$first_entry
=
array_shift
(
$entries
);
if
(!
$first_entry
instanceof
ClassElementEntry
)
{
throw
new
AssertionError
(
'Expected $entries of $property_descriptions to be a group of 1 or more entries'
);
}
$first_method
=
$first_entry
->
element
;
foreach
(
$entries
as
$entry
)
{
$method
=
$entry
->
element
;
self
::
emitPluginIssue
(
$this
->
code_base
,
$method
->
getContext
(),
"PhanPluginDuplicateMethodDescription"
,
"Method {METHOD} has the same description as the method {METHOD} on line {LINE}: {COMMENT}"
,
[
$method
->
getRepresentationForIssue
(),
$first_method
->
getName
()
.
'()'
,
$first_method
->
getContext
()->
getLineNumberStart
(),
$first_entry
->
description
]
);
}
}
return
[
$property_descriptions
,
$method_descriptions
];
}
/**
* @param list<ClassElementEntry> $values
* @return array<string, list<ClassElementEntry>>
*/
private
static
function
findGroups
(
array
$values
):
array
{
$result
=
[];
foreach
(
$values
as
$v
)
{
if
(
$v
->
element
->
isDeprecated
())
{
continue
;
}
$result
[
$v
->
description
][]
=
$v
;
}
foreach
(
$result
as
$description
=>
$keys
)
{
if
(
count
(
$keys
)
<=
1
)
{
unset
(
$result
[
$description
]);
}
}
return
$result
;
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return
new
HasPHPDocPlugin
();
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Aug 19 2026, 18:45 (6 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
f6/30/32d2f1d34cedb7bfe45673e053e8
Default Alt Text
HasPHPDocPlugin.php (14 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment