Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ext-* #118

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 116 additions & 46 deletions src/Analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@
class Analyser
{

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

/**
* @var Stopwatch
*/
Expand All @@ -67,7 +80,7 @@ class Analyser
private $classmap = [];

/**
* package name => is dev dependency
* package or ext-* => is dev dependency
*
* @var array<string, bool>
*/
Expand All @@ -81,15 +94,22 @@ class Analyser
private $ignoredSymbols;

/**
* function name => path
* custom function name => path
*
* @var array<string, string>
*/
private $definedFunctions = [];

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

/**
* @param array<string, ClassLoader> $classLoaders vendorDir => ClassLoader (e.g. result of \Composer\Autoload\ClassLoader::getRegisteredLoaders())
* @param array<string, bool> $composerJsonDependencies package name => is dev dependency
* @param array<string, bool> $composerJsonDependencies package or ext-* => is dev dependency
*/
public function __construct(
Stopwatch $stopwatch,
Expand Down Expand Up @@ -124,7 +144,7 @@ public function run(): AnalysisResult
$prodOnlyInDevErrors = [];
$unusedErrors = [];

$usedPackages = [];
$usedDependencies = [];
$prodPackagesUsedInProdPath = [];

$usages = [];
Expand All @@ -142,60 +162,65 @@ public function run(): AnalysisResult
continue;
}

$symbolPath = $this->getSymbolPath($usedSymbol, $kind);
if (isset($this->extensionSymbols[$kind][$usedSymbol])) {
$dependencyName = $this->extensionSymbols[$kind][$usedSymbol];

} else {
$symbolPath = $this->getSymbolPath($usedSymbol, $kind);

if ($symbolPath === null) {
if ($kind === SymbolKind::CLASSLIKE && !$ignoreList->shouldIgnoreUnknownClass($usedSymbol, $filePath)) {
foreach ($lineNumbers as $lineNumber) {
$unknownClassErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
if ($symbolPath === null) {
if ($kind === SymbolKind::CLASSLIKE && !$ignoreList->shouldIgnoreUnknownClass($usedSymbol, $filePath)) {
foreach ($lineNumbers as $lineNumber) {
$unknownClassErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
}
}
}

if ($kind === SymbolKind::FUNCTION && !$ignoreList->shouldIgnoreUnknownFunction($usedSymbol, $filePath)) {
foreach ($lineNumbers as $lineNumber) {
$unknownFunctionErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
if ($kind === SymbolKind::FUNCTION && !$ignoreList->shouldIgnoreUnknownFunction($usedSymbol, $filePath)) {
foreach ($lineNumbers as $lineNumber) {
$unknownFunctionErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
}
}

continue;
}

continue;
}
if (!$this->isVendorPath($symbolPath)) {
continue; // local class
}

if (!$this->isVendorPath($symbolPath)) {
continue; // local class
$dependencyName = $this->getPackageNameFromVendorPath($symbolPath);
}

$packageName = $this->getPackageNameFromVendorPath($symbolPath);

if (
$this->isShadowDependency($packageName)
&& !$ignoreList->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, $filePath, $packageName)
$this->isShadowDependency($dependencyName)
&& !$ignoreList->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, $filePath, $dependencyName)
) {
foreach ($lineNumbers as $lineNumber) {
$shadowErrors[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
$shadowErrors[$dependencyName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
}
}

if (
!$isDevFilePath
&& $this->isDevDependency($packageName)
&& !$ignoreList->shouldIgnoreError(ErrorType::DEV_DEPENDENCY_IN_PROD, $filePath, $packageName)
&& $this->isDevDependency($dependencyName)
&& !$ignoreList->shouldIgnoreError(ErrorType::DEV_DEPENDENCY_IN_PROD, $filePath, $dependencyName)
) {
foreach ($lineNumbers as $lineNumber) {
$devInProdErrors[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
$devInProdErrors[$dependencyName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
}
}

if (
!$isDevFilePath
&& !$this->isDevDependency($packageName)
&& !$this->isDevDependency($dependencyName)
) {
$prodPackagesUsedInProdPath[$packageName] = true;
$prodPackagesUsedInProdPath[$dependencyName] = true;
}

$usedPackages[$packageName] = true;
$usedDependencies[$dependencyName] = true;

foreach ($lineNumbers as $lineNumber) {
$usages[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
$usages[$dependencyName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
}
}
}
Expand All @@ -208,19 +233,31 @@ public function run(): AnalysisResult
continue;
}

$symbolPath = $this->getSymbolPath($forceUsedSymbol, null);
if (
isset($this->extensionSymbols[SymbolKind::FUNCTION][$forceUsedSymbol])
|| isset($this->extensionSymbols[SymbolKind::CONSTANT][$forceUsedSymbol])
|| isset($this->extensionSymbols[SymbolKind::CLASSLIKE][$forceUsedSymbol])
) {
$forceUsedDependency = $this->extensionSymbols[SymbolKind::FUNCTION][$forceUsedSymbol]
?? $this->extensionSymbols[SymbolKind::CONSTANT][$forceUsedSymbol]
?? $this->extensionSymbols[SymbolKind::CLASSLIKE][$forceUsedSymbol];
} else {
$symbolPath = $this->getSymbolPath($forceUsedSymbol, null);

if ($symbolPath === null || !$this->isVendorPath($symbolPath)) {
continue;
}

if ($symbolPath === null || !$this->isVendorPath($symbolPath)) {
continue;
$forceUsedDependency = $this->getPackageNameFromVendorPath($symbolPath);
}

$forceUsedPackage = $this->getPackageNameFromVendorPath($symbolPath);
$usedPackages[$forceUsedPackage] = true;
$forceUsedPackages[$forceUsedPackage] = true;
$usedDependencies[$forceUsedDependency] = true;
$forceUsedPackages[$forceUsedDependency] = true;
}

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

} else {
$dependenciesForUnusedAnalysis = array_keys(array_filter($this->composerJsonDependencies, static function (bool $devDependency) {
return !$devDependency; // dev deps are typically used only in CI
Expand All @@ -229,7 +266,8 @@ public function run(): AnalysisResult

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

foreach ($unusedDependencies as $unusedDependency) {
Expand All @@ -245,7 +283,8 @@ public function run(): AnalysisResult
$prodDependencies,
array_keys($prodPackagesUsedInProdPath),
array_keys($forceUsedPackages), // we dont know where are those used, lets not report them
$unusedDependencies
$unusedDependencies,
self::CORE_EXTENSIONS
);

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

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

/**
Expand Down Expand Up @@ -473,20 +515,40 @@ 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) {
foreach ($functionNames as $functionName) {
$reflectionFunction = new ReflectionFunction($functionName);
$functionFilePath = $reflectionFunction->getFileName();

if ($reflectionFunction->getExtension() === null && is_string($functionFilePath)) {
$this->definedFunctions[$functionName] = Path::normalize($functionFilePath);
if ($reflectionFunction->getExtension() === null) {
if (is_string($functionFilePath)) {
$this->definedFunctions[$functionName] = Path::normalize($functionFilePath);
}
} else {
$this->ignoredSymbols[$functionName] = true;
$extensionName = 'ext-' . $reflectionFunction->getExtension()->name;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the extension name should be always normalized to lowercase

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and composer also normalizes spaces to dashes: maglnet/ComposerRequireChecker#99


if (in_array($extensionName, self::CORE_EXTENSIONS, true)) {
$this->ignoredSymbols[$functionName] = true;
} else {
$this->extensionSymbols[SymbolKind::FUNCTION][$functionName] = $extensionName;
}
}
}
}
Expand All @@ -499,8 +561,16 @@ private function initExistingSymbols(): void

foreach ($classLikes as $classLikeNames) {
foreach ($classLikeNames as $classLikeName) {
if ((new ReflectionClass($classLikeName))->getExtension() !== null) {
$this->ignoredSymbols[$classLikeName] = true;
$classReflection = new ReflectionClass($classLikeName);

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

if (in_array($extensionName, self::CORE_EXTENSIONS, true)) {
$this->ignoredSymbols[$classLikeName] = true;
} else {
$this->extensionSymbols[SymbolKind::CLASSLIKE][$classLikeName] = $extensionName;
}
}
}
}
Expand Down
9 changes: 7 additions & 2 deletions src/ComposerJson.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class ComposerJson
public $composerAutoloadPath;

/**
* Package => isDev
* Package or ext-* => isDev
*
* @readonly
* @var array<string, bool>
Expand Down Expand Up @@ -73,13 +73,18 @@ public function __construct(
$this->extractAutoloadPaths($basePath, $composerJsonData['autoload-dev']['classmap'] ?? [], true)
);

$filterExtensions = static function (string $dependency): bool {
return strpos($dependency, 'ext-') === 0;
};
$filterPackages = static function (string $package): bool {
return strpos($package, '/') !== false;
};

$this->dependencies = array_merge(
array_fill_keys(array_keys(array_filter($requiredPackages, $filterPackages, ARRAY_FILTER_USE_KEY)), false),
array_fill_keys(array_keys(array_filter($requiredDevPackages, $filterPackages, ARRAY_FILTER_USE_KEY)), true)
array_fill_keys(array_keys(array_filter($requiredPackages, $filterExtensions, ARRAY_FILTER_USE_KEY)), false),
array_fill_keys(array_keys(array_filter($requiredDevPackages, $filterPackages, ARRAY_FILTER_USE_KEY)), true),
array_fill_keys(array_keys(array_filter($requiredDevPackages, $filterExtensions, ARRAY_FILTER_USE_KEY)), true)
);

if (count($this->dependencies) === 0) {
Expand Down
30 changes: 24 additions & 6 deletions src/UsedSymbolExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace ShipMonk\ComposerDependencyAnalyser;

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 @@ -58,14 +61,25 @@ public function __construct(string $code)
* It does not produce any local names in current namespace
* - 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
public function parseUsedSymbols(
array $definedFunctions,
array $definedConstants
): array
{
$usedSymbols = [];
$useStatements = [];
$useStatementKinds = [];
$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 @@ -96,7 +110,9 @@ public function parseUsedSymbols(): array
break;

case PHP_VERSION_ID >= 80000 ? T_NAMESPACE : -1:
$useStatements = []; // reset use statements on namespace change
// reset use statements on namespace change
$useStatements = $initialSseStatements;
$useStatementKinds = $initialUseStatementKinds;
break;

case PHP_VERSION_ID >= 80000 ? T_NAME_FULLY_QUALIFIED : -1:
Expand Down Expand Up @@ -132,7 +148,9 @@ public function parseUsedSymbols(): array
$nextName = $this->parseNameForOldPhp();

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

break;
Expand Down Expand Up @@ -185,7 +203,7 @@ public function parseUsedSymbols(): 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
Loading
Loading