Skip to content

Commit

Permalink
Track extension constants
Browse files Browse the repository at this point in the history
  • Loading branch information
janedbal committed Apr 10, 2024
1 parent f543765 commit 6cd4c6f
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 56 deletions.
102 changes: 55 additions & 47 deletions src/Analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use function array_filter;
use function array_key_exists;
use function array_keys;
use function array_merge;
use function explode;
use function file_get_contents;
use function get_declared_classes;
Expand All @@ -43,16 +44,16 @@ class Analyser
{

private const CORE_EXTENSIONS = [
'Core',
'date',
'json',
'hash',
'pcre',
'Phar',
'Reflection',
'SPL',
'random',
'standard',
'ext-Core',
'ext-date',
'ext-json',
'ext-hash',
'ext-pcre',
'ext-Phar',
'ext-Reflection',
'ext-SPL',
'ext-random',
'ext-standard',
];

/**
Expand Down Expand Up @@ -108,18 +109,11 @@ class Analyser
private $definedFunctions = [];

/**
* class name => ext-*
* kind => [symbol name => ext-*]
*
* @var array<string, string>
* @var array<SymbolKind::*, array<string, string>>
*/
private $extensionClasses;

/**
* function name => ext-*
*
* @var array<string, string>
*/
private $extensionFunctions;
private $extensionSymbols;

/**
* @param array<string, ClassLoader> $classLoaders vendorDir => ClassLoader (e.g. result of \Composer\Autoload\ClassLoader::getRegisteredLoaders())
Expand Down Expand Up @@ -163,6 +157,7 @@ public function run(): AnalysisResult
$unusedErrors = [];

$usedPackages = [];
$usedExtensions = [];
$prodPackagesUsedInProdPath = [];

$usages = [];
Expand All @@ -180,20 +175,9 @@ public function run(): AnalysisResult
continue;
}

if ($kind === SymbolKind::FUNCTION && isset($this->extensionFunctions[$usedSymbol])) {
$neededExtension = $this->extensionFunctions[$usedSymbol];

if (!isset($this->composerJsonExtensions[$neededExtension])) {
foreach ($lineNumbers as $lineNumber) {
$missingExtensions[$neededExtension][] = new SymbolUsage($filePath, $lineNumber, $kind);
}
}

continue;
}

if ($kind === SymbolKind::CLASSLIKE && isset($this->extensionClasses[$usedSymbol])) {
$neededExtension = $this->extensionClasses[$usedSymbol];
if (isset($this->extensionSymbols[$kind][$usedSymbol])) {
$neededExtension = $this->extensionSymbols[$kind][$usedSymbol];
$usedExtensions[$neededExtension] = true;

if (!isset($this->composerJsonExtensions[$neededExtension])) {
foreach ($lineNumbers as $lineNumber) {
Expand Down Expand Up @@ -282,16 +266,25 @@ public function run(): AnalysisResult
}

if ($this->config->shouldReportUnusedDevDependencies()) {
$dependenciesForUnusedAnalysis = array_keys($this->composerJsonDependencies);
$dependenciesForUnusedAnalysis = array_merge(
array_keys($this->composerJsonDependencies),
array_keys($this->composerJsonExtensions)
);

} else {
$dependenciesForUnusedAnalysis = array_keys(array_filter($this->composerJsonDependencies, static function (bool $devDependency) {
return !$devDependency; // dev deps are typically used only in CI
}));
$dependenciesForUnusedAnalysis = array_merge(
array_keys(array_filter($this->composerJsonDependencies, static function (bool $devDependency) {
return !$devDependency; // dev deps are typically used only in CI
})),
array_keys($this->composerJsonExtensions)
);
}

$unusedDependencies = array_diff(
$dependenciesForUnusedAnalysis,
array_keys($usedPackages)
array_keys($usedPackages),
array_keys($usedExtensions),
self::CORE_EXTENSIONS
);

foreach ($unusedDependencies as $unusedDependency) {
Expand Down Expand Up @@ -391,7 +384,10 @@ private function getUsedSymbolsInFile(string $filePath): array
throw new InvalidPathException("Unable to get contents of '$filePath'");
}

return (new UsedSymbolExtractor($code))->parseUsedSymbols($this->extensionFunctions);
return (new UsedSymbolExtractor($code))->parseUsedSymbols(
$this->extensionSymbols[SymbolKind::FUNCTION],
$this->extensionSymbols[SymbolKind::CONSTANT]
);
}

/**
Expand Down Expand Up @@ -536,9 +532,21 @@ private function initExistingSymbols(): void
'Composer\\Autoload\\ClassLoader' => true,
];

/** @var string $constantName */
foreach (get_defined_constants() as $constantName => $constantValue) {
$this->ignoredSymbols[$constantName] = true;
/** @var array<string, mixed> $constants */
foreach (get_defined_constants(true) as $constantExtension => $constants) {
foreach ($constants as $constantName => $_) {
if ($constantExtension === 'user') {
$this->ignoredSymbols[$constantName] = true;
} else {
$extensionName = 'ext-' . $constantExtension;

if (in_array($extensionName, self::CORE_EXTENSIONS, true)) {
$this->ignoredSymbols[$constantName] = true;
} else {
$this->extensionSymbols[SymbolKind::CONSTANT][$constantName] = $extensionName;
}
}
}
}

foreach (get_defined_functions() as $functionNames) {
Expand All @@ -551,12 +559,12 @@ private function initExistingSymbols(): void
$this->definedFunctions[$functionName] = Path::normalize($functionFilePath);
}
} else {
$extensionName = $reflectionFunction->getExtension()->name;
$extensionName = 'ext-' . $reflectionFunction->getExtension()->name;

if (in_array($extensionName, self::CORE_EXTENSIONS, true)) {
$this->ignoredSymbols[$functionName] = true;
} else {
$this->extensionFunctions[$functionName] = 'ext-' . $extensionName;
$this->extensionSymbols[SymbolKind::FUNCTION][$functionName] = $extensionName;
}
}
}
Expand All @@ -573,12 +581,12 @@ private function initExistingSymbols(): void
$classReflection = new ReflectionClass($classLikeName);

if ($classReflection->getExtension() !== null) {
$extensionName = $classReflection->getExtension()->name;
$extensionName = 'ext-' . $classReflection->getExtension()->name;

if (in_array($extensionName, self::CORE_EXTENSIONS, true)) {
$this->ignoredSymbols[$classLikeName] = true;
} else {
$this->extensionClasses[$classLikeName] = 'ext-' . $extensionName;
$this->extensionSymbols[SymbolKind::CLASSLIKE][$classLikeName] = $extensionName;
}
}
}
Expand Down
27 changes: 19 additions & 8 deletions src/UsedSymbolExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use function array_combine;
use function array_fill_keys;
use function array_merge;
use function count;
use function explode;
use function is_array;
Expand Down Expand Up @@ -61,14 +62,24 @@ public function __construct(string $code)
* - this results in very limited functionality in files without namespace
*
* @param array<string> $definedFunctions
* @param array<string> $definedConstants
* @return array<SymbolKind::*, array<string, list<int>>>
* @license Inspired by https://github.com/doctrine/annotations/blob/2.0.0/lib/Doctrine/Common/Annotations/TokenParser.php
*/
public function parseUsedSymbols(array $definedFunctions): array
public function parseUsedSymbols(
array $definedFunctions,
array $definedConstants
): array
{
$usedSymbols = [];
$useStatements = array_combine($definedFunctions, $definedFunctions);
$useStatementKinds = array_fill_keys($definedFunctions, SymbolKind::FUNCTION);
$useStatements = $initialSseStatements = array_merge(
array_combine($definedFunctions, $definedFunctions),
array_combine($definedConstants, $definedConstants)
);
$useStatementKinds = $initialUseStatementKinds = array_merge(
array_fill_keys($definedFunctions, SymbolKind::FUNCTION),
array_fill_keys($definedConstants, SymbolKind::CONSTANT)
);

$level = 0;
$inClassLevel = null;
Expand Down Expand Up @@ -100,8 +111,8 @@ public function parseUsedSymbols(array $definedFunctions): array

case PHP_VERSION_ID >= 80000 ? T_NAMESPACE : -1:
// reset use statements on namespace change
$useStatements = array_combine($definedFunctions, $definedFunctions);
$useStatementKinds = array_fill_keys($definedFunctions, SymbolKind::FUNCTION);
$useStatements = $initialSseStatements;
$useStatementKinds = $initialUseStatementKinds;
break;

case PHP_VERSION_ID >= 80000 ? T_NAME_FULLY_QUALIFIED : -1:
Expand Down Expand Up @@ -138,8 +149,8 @@ public function parseUsedSymbols(array $definedFunctions): array

if (substr($nextName, 0, 1) !== '\\') { // not a namespace-relative name, but a new namespace declaration
// reset use statements on namespace change
$useStatements = array_combine($definedFunctions, $definedFunctions);
$useStatementKinds = array_fill_keys($definedFunctions, SymbolKind::FUNCTION);
$useStatements = $initialSseStatements;
$useStatementKinds = $initialUseStatementKinds;
}

break;
Expand Down Expand Up @@ -192,7 +203,7 @@ public function parseUsedSymbols(array $definedFunctions): array
}
}

return $usedSymbols; // @phpstan-ignore-line Not enough precise analysis "Offset 'kind' (1|2|3) does not accept type int<1, max>"
return $usedSymbols;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/UsedSymbolExtractorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function test(string $path, array $expectedUsages): void

$extractor = new UsedSymbolExtractor($code);

self::assertSame($expectedUsages, $extractor->parseUsedSymbols(['json_encode']));
self::assertSame($expectedUsages, $extractor->parseUsedSymbols(['json_encode'], []));
}

/**
Expand Down

0 comments on commit 6cd4c6f

Please sign in to comment.