Targeted analysis hooks

Targeted analysis hooks inspect selected syntax or semantic targets after their containing file has completed analysis. Mago performs target matching natively. A host that registers only targeted hooks receives compact batches for matching files; when the same host also registers plain after-file hooks, their batches already contain every analyzed file and targeted callbacks are skipped inside unmatched files.

Place targeted hook implementations under src/Mago/Analyzer/Hooks/.

All targeted hooks implement the same contract:

interface TargetedAnalysisHook
{
    public function getTargets(): array;

    public function getRequirements(): array;

    public function analyze(NodeAnalysisContext $context): void;
}

Hook kinds

Syntax nodes

NodeAnalysisHook returns NodeKind targets. Use it when the rule depends on a concrete PHP construct regardless of resolved symbols:

public function getTargets(): array
{
    return [NodeKind::Attribute, NodeKind::Class_];
}

Resolved method calls

MethodCallAnalysisHook returns MethodTarget values. Mago resolves the receiver hierarchy and method name before matching, so the extension does not need to inspect every call:

public function getTargets(): array
{
    return [MethodTarget::exact(Builder::class, 'whereRaw')];
}

Descendant declarations

ClassLikeAnalysisHook returns ClassLikeTarget::descendantsOf() values. It receives a class-like declaration when the named ancestor appears in that declaration's populated transitive parent-class or parent-interface metadata. The ancestor declaration itself never matches.

public function getTargets(): array
{
    return [ClassLikeTarget::descendantsOf(Command::class)];
}

Request data explicitly

The hook's FileAnalysisRequirement list controls which expensive artifacts Mago embeds. Mago unions the requirements of hooks that can share a target, so a context may contain data requested by another matching hook:

public function getRequirements(): array
{
    return [
        FileAnalysisRequirement::TargetExpressionTypes,
        FileAnalysisRequirement::ReceiverType,
        FileAnalysisRequirement::ArgumentTypes,
        FileAnalysisRequirement::TargetSubtree,
    ];
}
RequirementNodeAnalysisContext effect
TargetExpressionTypesPopulates targetType when Mago inferred one
ReceiverTypePopulates receiverType for direct method and static calls
ArgumentTypesPopulates argumentTypes in source order; individual entries may be null
TargetSubtreeRetains the target's concrete-syntax descendants in source
SourceTextRetains exact in-memory file bytes and all comment trivia in source
ExpressionTypesEmbeds every file expression type in analysis; generally an after-file concern

Without TargetSubtree, each selected target is retained as a standalone node without its ancestors or descendants. With TargetSubtree, that target's descendants are retained too. Without SourceText, SourceFile::$contents is an empty string and comment trivia is omitted. Resolved names are limited to the retained target ranges. If a hook only needs the node kind, span, and semantic type, request neither.

Analysis context

NodeAnalysisContext contains:

MemberDescription
sourceFiltered SourceFile snapshot containing the selected node
nodeCurrent target node
analysisCompleted FileAnalysis for the containing file
referencesShared file-scoped ReferenceRegistry
targetTypeRequested target expression type, or null
receiverTypeRequested direct receiver type, or null
argumentTypesRequested direct argument types, otherwise an empty list
lifecycle membersPHP version, codebase, type comparator, cancellation, and report()

Within one plugin callback, the plugin's after-file hooks run first and its targeted hooks then share that same file-scoped reference registry. Different plugins receive separate registries; Mago merges every plugin's contributions with the file result and replaces them if that file is reanalyzed.

Example

<?php

declare(strict_types=1);

namespace Acme\Mago\Analyzer\Hooks;

use Acme\Framework\Database\Builder;
use Mago\Sdk\Analyzer\FileAnalysisRequirement;
use Mago\Sdk\Analyzer\MethodCallAnalysisHook;
use Mago\Sdk\Analyzer\MethodTarget;
use Mago\Sdk\Analyzer\NodeAnalysisContext;
use Mago\Sdk\Reporting\Issue;
use Mago\Sdk\Reporting\Level;

final class UnsafeQueryHook implements MethodCallAnalysisHook
{
    public function getTargets(): array
    {
        return [MethodTarget::exact(Builder::class, 'whereRaw')];
    }

    public function getRequirements(): array
    {
        return [FileAnalysisRequirement::ArgumentTypes];
    }

    public function analyze(NodeAnalysisContext $context): void
    {
        $sql = $context->argumentTypes[0] ?? null;
        if ($sql?->getLiteralString() !== null) {
            return;
        }

        $context->report(
            Level::Warning,
            'non-literal-query',
            Issue::new('Use a literal query or a parameterized builder API.', $context->node->span),
        );
    }
}

Avoid duplicate work

  • Prefer MethodCallAnalysisHook over a broad call-node hook when symbol resolution determines applicability.
  • Prefer ClassLikeAnalysisHook over scanning every class and querying ancestry in PHP.
  • Request the smallest artifact set.
  • Do not traverse descendants unless TargetSubtree was requested and the check requires them.
  • Batch metadata, expression-type, and comparison lookups.
  • Return quickly for the common non-match even after native targeting.

↳ Edit this page →