Phan comes with a mechanism for adding plugins for your code base. Plugins have hooks for analyzing every node in the AST for every file, classes, methods and functions. (As well as analyzing param types or return types pf functions when they are invoked. And analyzing functions/constants/classes/class elements that Phan will analyze.)
Plugin code lives in your repo and is referenced from your phan config file, but runs in the Phan execution environment with access to all Phan APIs.
PluginV3 is the current version of the plugin system.
The system is designed to be extensible and as efficient as possible. (e.g. plugins would only be invoked for the functionality they implement)
The constant \Phan\Config::PHAN_PLUGIN_VERSION may optionally be used by plugin files designed for backwards compatibility.
version_compare may be used to check if the current plugin system is >= the version where a given Capability was introduced or changed.
To create a plugin, you'll need to
- Create a plugin file for which the last line returns an instance of a class extending
\Phan\PluginV3, and implementing one or more of theCapabilityinterfaces - Add a reference to the file in
.phan/config.phpunder thepluginsarray.
Phan contains an example plugin named DemoPlugin that is referenced from Phan's .phan/config.php file.
A more meaningful real-world example is given in DollarDollarPlugin which checks to make sure there are no variables of the form $$var in Phan's code base.
You may wish to base your plugin on a plugin performing a similar task. (list of plugins)
- Additionally, you may wish to base code on other functionality that Phan implements internally as plugins
A plugin file returns an instance of a class extending \Phan\PluginV3 and should implement at least one of the below Capability interfaces (The most up to date documentation is found in \Phan\PluginV3).
- \Phan\PluginV3\PostAnalyzeNodeCapability
to analyze a subset of node kinds after child nodes of that node are analyzed. (e.g. DuplicateArrayKeyPlugin uses this for nodes of the kind
AST_ARRAY) - \Phan\PluginV3\AnalyzeFunctionCallCapability
can be used to run additional checks of the parameters of function or method invocations. (E.g. PregRegexChecker does this for
preg_match, etc to check regex uses) - \Phan\PluginV3\ReturnTypeOverrideCapability
can be used to manipulate the return types of a function or method call based on its arguments and other information.
(E.g. Phan uses this for
json_decode,var_export, etc - \Phan\PluginV3\AnalyzeClassCapability will call a plugin to analyze every class declaration. (e.g. UnusedSuppressionPlugin uses this to check if suppression doc comments on classes are used)
- \Phan\PluginV3\AnalyzeMethodCapability will call a plugin to analyze every method declaration. (e.g. AlwaysReturnPlugin uses this to check if a method is guaranteed to return)
- \Phan\PluginV3\AnalyzeFunctionCapability will call a plugin to analyze every function declaration. (e.g. AlwaysReturnPlugin uses this to check if a function is guaranteed to return)
- \Phan\PluginV3\AnalyzePropertyCapability will call a plugin to analyze every property declaration. (e.g. UnusedSuppressionPlugin uses this to check if suppression doc comments on a property are used)
- \Phan\PluginV3\FinalizeProcessCapability
will call
finalize(CodeBase)once after each analysis process (as in--processes N) ends. (e.g. UnusedSuppressionPlugin will defer emitting warnings untilfinalize(...), which indicates that Phan has analyzed every single file and emitted all possible issues) - \Phan\PluginV3\PreAnalyzeNodeCapability
to analyze a subset of node kinds before child nodes of that node are analyzed. (e.g. DuplicateArrayKeyPlugin uses this for nodes of the kind
AST_ARRAY) Most plugins will use PostAnalyzeNodeCapability instead of PreAnalyzeNodeCapability. - \Phan\PluginV3\SuppressionCapability
can be used to suppress issues based on custom logic.
(e.g.
BuiltinSuppressionPluginimplements@phan-suppress-next-line, etc) - \Phan\PluginV3\BeforeAnalyzeCapability
provides
beforeAnalyze(CodeBase), called before Phan begins analyzing methods — before forking worker processes. Use this to set up global state your plugin needs before analysis begins. When unsure whether to useBeforeAnalyzeCapabilityorBeforeAnalyzePhaseCapability, preferBeforeAnalyzePhaseCapability. - \Phan\PluginV3\BeforeAnalyzePhaseCapability
provides
beforeAnalyzePhase(CodeBase), called after Phan has analyzed all method/function signatures but before forking workers for file analysis. This is the preferred hook for pre-analysis setup in most plugins. - \Phan\PluginV3\BeforeAnalyzeFileCapability
provides
beforeAnalyzeFile(CodeBase, Context, string $file_contents, Node), called before Phan analyzes each file. The absolute path is available viaConfig::projectPath($context->getFile()). Not called for empty files. - \Phan\PluginV3\AfterAnalyzeFileCapability
provides
afterAnalyzeFile(CodeBase, Context, string $file_contents, Node), called after Phan has finished analyzing each file. Not called for empty files. - \Phan\PluginV3\AutomaticFixCapability
provides
getAutomaticFixers(), which returns a map from issue type name to a closure that produces aFileEditSetdescribing the bytes to change. Used with--automatic-fix. See Implementing Automatic Fixes below. - \Phan\PluginV3\AnalyzeLiteralStatementCapability
provides
analyzeStringLiteralStatement(CodeBase, Context, string $statement): bool, called when Phan encounters a no-op string literal expression (e.g.'@phan-debug-var $x'). Returntrueif your plugin consumed the statement. This is the mechanism Phan uses internally for@phan-debug-var. - \Phan\PluginV3\SubscribeEmitIssueCapability
provides
onEmitIssue(IssueInstance): bool, called before every unsuppressed issue is emitted. Returntrueto suppress the issue. PreferSuppressionCapabilityfor most suppression use cases —SubscribeEmitIssueCapabilityhas higher overhead because it is called for every issue. - \Phan\PluginV3\BeforeLoopBodyAnalysisCapability
provides
getBeforeLoopBodyAnalysisVisitorClassName(), returning the name of a visitor class extendingBeforeLoopBodyAnalysisVisitor. Called to analyze loop conditions before Phan analyzes the loop body. - \Phan\PluginV3\MergeVariableInfoCapability
provides
getMergeVariableInfoClosure(), returning a closure that customizes how variable types are merged across branches (if/else, try/catch). Useful for plugins that track custom per-variable state. - \Phan\PluginV3\HandleLazyLoadInternalFunctionCapability
provides
handleLazyLoadInternalFunction(CodeBase, Func), called when Phan lazily loads a global internal PHP function during analysis. Useful for modifying lazily-loaded functions thatAnalyzeFunctionCallCapabilitycan't intercept.
As the method names suggest, they analyze (Or return the class name of a Visitor that will analyze) AST nodes, classes, methods and functions; and pre-analyze AST nodes, respectively.
When issues are found, they can be emitted to the log via a call to emitIssue.
$this->emitIssue(
$code_base,
$context,
'PhanPluginMyPluginType',
"Issue message template associated with the issue.",
[] // optional template args
);where $code_base is the CodeBase object passed to your hook, $context is the context in which the issue is found (such as the $context passed to the hook, or from $class->getContext(), $method->getContext() or $function->getContext(), a name for the issue type (allowing it to be suppressed via @suppress) and the message to emit to the user.
The emitted issues may also have format strings (same as recent releases of Phan with V1 support), and the same types of format strings as \Phan\Issue (E.g. in \UnusedSuppressionPlugin
$this->emitIssue(
$code_base,
$element->getContext(),
'UnusedSuppression',
"Element {FUNCTIONLIKE} suppresses issue {ISSUETYPE} but does not use it", // This type of format string lets ./phan --color colorize the output
[(string)$element->getFQSEN(), $issue_type]
);A shorthand was added to emit issues from visitors such as PluginAwareAnalysisVisitor or PluginAwarePreAnalysisVisitor, via $this->emit(issue type, format string, [args...], [...]) (Implicitly uses global codebase and current context),
or $this->emitPluginIssue(CodeBase, Context, issue type, format string, [args...], [...]). See InvalidVariableIssetVisitor for an example of this.
When writing plugins, you'll likely need to understand a few concepts. The following contains some material that may be useful. You can learn more from the Developer's Guide to Phan
A Node is an AST node returned from the php-ast PHP extension. You can read more about its interface in its README. You'll also find many references to Node that can be copied throughout the Phan code base.
The Clazz class contains things you'll need to know about a class such as its FQSEN (fully-qualified structural element name), name, type, context, and flags such as isAbstract, isInterface, isTrait, etc.
The Method class contains things you'll need to know about methods such as its FQSEN, name, parameters, return type, etc..
Similarly, the Func class contains things you'll need to know about functions.
A Context is a thing defined for every line of code that tells you which file you're in, which line you're on, which class you're in, which methods, function or closure you're in and the Scope that is available to you which contains all local and global variables.
A UnionType is a set of Types defined for an object such as int|string|DateTime|null. You can read more about UnionTypes here.
You'll likely find yourself getting types frequently via a call to UnionTypeVisitor::unionTypeFromNode(...) such as with:
$union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node);which provides you with the union type of the statement defined by the AST node $node.
A ContextNode contains a lot of useful functionality, such as locating the definition of an element from a reference (class, function-like, property, etc) in a Context,
or getting an equivalent PHP value for a given Node.
(PregRegexCheckerPlugin uses that).
A CodeBase is a thing containing a mapping from all FQSENs (fully qualified structural element names) to their associated objects (Classes, Methods, Functions, Properties, Constants).
You can use the CodeBase to look up info on any objects you find references to.
More typically, you'll need to keep the $code_base around to pass it to all of Phan's methods.
Plugins can support the --automatic-fix CLI flag by implementing AutomaticFixCapability. When Phan runs with --automatic-fix, it calls each fixer closure with the issue instance and the cached file content, then applies the returned byte-range edits in-place.
Note: Automatic fix requires the polyfill parser for source location information. Run Phan with
--force-polyfill-parser-with-original-tokenswhen using--automatic-fix.
getAutomaticFixers() returns a map from issue type name to a fixer closure:
use Phan\Plugin\Internal\IssueFixingPlugin\FileEditSet;
class MyPlugin extends PluginV3 implements AutomaticFixCapability
{
public function getAutomaticFixers(): array
{
return [
'PhanPluginMyIssue' => static function (
CodeBase $code_base,
FileCacheEntry $cached_file,
IssueInstance $instance
): ?FileEditSet {
// Return null if this instance can't be fixed automatically.
// Otherwise return a FileEditSet describing the bytes to change.
$line = $instance->getLine();
// ... locate the bytes to change using $cached_file ...
return new FileEditSet([
new FileEdit($byte_offset, $byte_length, $replacement_string),
]);
},
];
}
}FileEdit($offset, $length, $replacement)— replace$lengthbytes starting at$offsetwith$replacement. Use$replacement = ''to delete.FileEditSet($edits)— a collection ofFileEditobjects. Edits must not overlap.
PHPDocRedundantPlugin implements AutomaticFixCapability and delegates to PHPDocRedundantPlugin/Fixers.php, which uses the tolerant-php-parser to locate and delete redundant doc comment tokens.