Analýzy: NameResolver a Scope
Dvě vestavěné analýzy odpovídají na otázky, které si klade každý nástroj pracující s kódem: co dané jméno doopravdy znamená a v jaké funkci a třídě kód stojí.
Co je analýza
Strom říká, jak kód vypadá. Analýza je informace odvozená z celého souboru, kterou strom sám nenese: co znamená
jméno Order v tomhle souboru, jestli je $this k dispozici, které proměnné closure zachycuje. Je to
obyčejná třída postavená nad FileNode; nic se neregistruje a žádné rozhraní se neimplementuje.
$resolver = new PhpSyntax\Analyses\NameResolver($file);
$scope = new PhpSyntax\Analyses\Scope;
Analýza, která si odvozené informace pamatuje, čte strom tak, jak vypadal, když vznikla; po změně stromu si ji postavte
znovu, a že se strom změnil, poznáte podle FileNode::$revision. Týká se to NameResolveru.
Scope si nepamatuje nic a nebere ani soubor: každou otázku zodpoví procházkou po rodičích uzlu, který dostal,
takže ho žádná úprava nezastará.
NameResolver
Překládá jména podle jmenného prostoru a importů, s tím, co PHP dělá za běhu: nekvalifikovaná funkce nebo konstanta, která ve jmenném prostoru není, spadne do globálního.
Importy platí v bloku, ve kterém jsou napsané, kdežto deklarace patří celému jmennému prostoru. Druhý blok
namespace A { } téhož souboru proto vidí funkci, kterou deklaroval ten první, ale ne to, co si první blok
importoval. Co soubor nedeklaruje, o tom resolver sám neví nic: pád do globálního prostoru je pak odhad podle toho, co je
v souboru vidět, ne znalost symbolu. Z odhadu se stane znalost, když resolveru při vytvoření předáte objekt
NamespacedSymbols: seznam funkcí a konstant, které jmenné prostory deklarují mimo soubor, a příznak
complete, že jiné nejsou. Seznamy přitom mohou být prázdné, a přesně tak to vypadá v projektu, který ve
jmenných prostorech žádné funkce ani konstanty nemá. Jestli konkrétní odpověď stojí jen na odhadu, řekne
getUnqualifiedResolution().
$symbols = new PhpSyntax\Analyses\NamespacedSymbols(
functions: ['App\Utils\format'],
constants: ['App\VERSION'],
complete: true,
);
$resolver = new PhpSyntax\Analyses\NameResolver($file, $symbols);
$resolver->getNamespace($node); // 'App\Model', '' v globálním prostoru
$resolver->getClassImports($node); // alias => plné jméno, platné v místě uzlu
$resolver->getFunctionImports($node);
$resolver->getConstantImports($node);
$resolver->resolveClass($name); // 'App\Model\Order' z NameNode
$resolver->resolveFunction($name);
$resolver->resolveConstant($name);
$resolver->isGlobalFunctionCall($node, 'sizeof'); // volání globální funkce, případně té jedné
$resolver->getUnqualifiedResolution('count', PhpSyntax\SymbolKind::Function, $node); // Global, Namespaced, nebo Uncertain, když pád do globálního stojí jen na odhadu
isGlobalFunctionCall() je otázka, kterou si dřív každý nástroj řešil sám a většinou hůř: uzel musí
být volání funkce se jménem (ne $f() ani $obj->f()), jméno nesmí být klíčové slovo a po
překladu musí být globální. Tak se pozná, že count($a) uvnitř namespace App opravdu volá
count, a ne App\count, pokud taková funkce není importovaná, deklarovaná v souboru ani uvedená v
NamespacedSymbols.
Nástroj, který podle odpovědi mění kód, se má zeptat i na getUnqualifiedResolution(). Takhle rozhoduje DressCode, jestli je oprava riziková: s odpovědí Uncertain ano, s
Global nebo Namespaced ne. Metoda přijímá jen nekvalifikované jméno, protože jméno s lomítkem
do globálního prostoru nepadá, a na jiné vyhodí InvalidArgumentException. Odpověď Global přitom
říká, že jméno míří do globálního prostoru, ne nutně na symbol toho jména: import s aliasem ho může
přesměrovat jinam.
Jméno, které jste právě naparsovali a ve stromu ještě nestojí, samo neví, kde bude; kde ho chcete přeložit, řekne druhý argument:
$name = $parser->parseName('Strings');
$resolver->resolveClass($name, $at); // přeloží se tak, jak by platilo v místě $at
Uzel z cizího stromu je odmítnut výjimkou, ne zodpovězen v globálním jmenném prostoru. Tichá platná odpověď na nepoloženou otázku je horší než chyba.
Cesta zpátky je stejně užitečná: jak plné jméno zapsat v místě, kde stojí uzel.
$kind = PhpSyntax\SymbolKind::ClassLike;
$resolver->getShortName('Nette\Utils\Strings', $kind, $at); // 'Strings', je importované
$resolver->getShortName('App\Model\Order', $kind, $at); // 'Order'
$resolver->getShortName('Other\Thing', $kind, $at); // '\Other\Thing', jinak by se hledalo v namespace
$resolver->isAliasFree('Strings', $kind, $at); // false, ten alias už něco znamená
getShortName() vezme v úvahu aliasy, prefixové importy i jmenný prostor a vrátí nejkratší zápis, který
na tom místě znamená totéž. To „znamená totéž“ je tvrdá podmínka: kde je jméno obsazené importem, vrátí radši
delší zápis. S use function X\count; v souboru tak globální count dostane '\count',
protože holé count by na tom místě znamenalo X\count. Ze stejného důvodu dostane globální funkce
nebo konstanta ve jmenném prostoru holé jméno jen tehdy, když resolver dostal NamespacedSymbols s příznakem
complete; bez té znalosti by holé count mohlo znamenat App\count z jiného souboru.
Přečtením zpátky přes resolveFunction() a spol. se tedy vždycky vrátíte k symbolu, na který jste se ptali.
isAliasFree() odpoví na otázku, kterou si klade každý, kdo chce import doplnit: je tohle jméno tady
ještě volné?
A ještě jedna dvojice, tentokrát o deklaracích v souboru:
$resolver->getDeclaredName($class); // 'App\Cart' z uzlu deklarace
$resolver->findDeclaration('App\Cart', $kind); // ClassLikeNode, nebo null
findDeclaration() hledá mezi symboly jednoho druhu, protože PHP drží třídu, funkci a konstantu stejného
jména odděleně: SymbolKind::ClassLike najde třídu, rozhraní, trait nebo výčet, Function funkci a
Constant položku příkazu const. Velikost písmen bere tak jako PHP, tedy u konstanty ji rozlišuje
jen v jejím vlastním jméně, a úvodní backslash mu nevadí. getDeclaredName() vrátí jméno i pro takovou
konstantu; konstanta třídy do jmenného prostoru nic nezavádí, a proto dostane null.
Scope
Kde ve struktuře kód stojí:
$scope->getFunction($node); // FunctionLikeNode: funkce, metoda, closure, arrow funkce nebo hook, nebo null
$scope->getClass($node); // ClassLikeNode nebo null
$scope->hasThis($node); // je tu $this: v nestatické metodě, v closure, která ho zachytila
$scope->getCapturedVariables($closure); // proměnné z use (...)
hasThis() bere v úvahu i to, že closure $this zdědí, ale static function ne, a že
arrow funkce ho vidí vždy, když ho vidí okolí.
Vlastní analýza
Když víc míst potřebuje tutéž informaci o souboru, napište ji jako třídu s konstruktorem přijímajícím
FileNode. Nástroj postavený na PhpSyntax si takovou analýzu obvykle staví a zahazuje sám podle revize stromu; DressCode to dělá za pravidla a přidává k oběma vestavěným
ještě PhpDoc nad dokumentačními komentáři, protože ten už potřebuje parser phpDocu, který sem
nepatří.