diff --git a/src/ComposerRequireChecker/ASTLocator/FileAST.php b/src/ComposerRequireChecker/ASTLocator/FileAST.php new file mode 100644 index 00000000..20ff3c1e --- /dev/null +++ b/src/ComposerRequireChecker/ASTLocator/FileAST.php @@ -0,0 +1,45 @@ +file = $file; + $this->ast = $ast; + } + + /** + * @return string + */ + public function getFile() + { + return $this->file; + } + + /** + * @return Traversable|array + */ + public function getAst() + { + return $this->ast; + } +} diff --git a/src/ComposerRequireChecker/ASTLocator/LocateASTFromFiles.php b/src/ComposerRequireChecker/ASTLocator/LocateASTFromFiles.php index 7eebc726..1ca45b30 100644 --- a/src/ComposerRequireChecker/ASTLocator/LocateASTFromFiles.php +++ b/src/ComposerRequireChecker/ASTLocator/LocateASTFromFiles.php @@ -27,12 +27,12 @@ public function __construct(Parser $parser, ?ErrorHandler $errorHandler) /** * @param Traversable|string[] $files * - * @return Traversable|array[] a series of AST roots, one for each given file + * @return Traversable|FileAST[] a series of AST roots, one for each given file */ public function __invoke(Traversable $files): Traversable { foreach ($files as $file) { - yield $this->parser->parse(file_get_contents($file), $this->errorHandler); + yield new FileAST($file, $this->parser->parse(file_get_contents($file), $this->errorHandler)); } } } diff --git a/src/ComposerRequireChecker/Cli/CheckCommand.php b/src/ComposerRequireChecker/Cli/CheckCommand.php index 3cc4255f..94edfc05 100644 --- a/src/ComposerRequireChecker/Cli/CheckCommand.php +++ b/src/ComposerRequireChecker/Cli/CheckCommand.php @@ -67,12 +67,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $sourcesASTs = $this->getASTFromFilesLocator($input); - $definedVendorSymbols = (new LocateDefinedSymbolsFromASTRoots())->__invoke($sourcesASTs( + list($definedVendorSymbols, $additionalFiles) = (new LocateDefinedSymbolsFromASTRoots())->__invoke($sourcesASTs( (new ComposeGenerators())->__invoke( $getPackageSourceFiles($composerJson), (new LocateComposerPackageDirectDependenciesSourceFiles())->__invoke($composerJson) ) )); + while ($additionalFiles && count($additionalFiles)) { + list($vendorSymbols, $additionalFiles) = (new LocateDefinedSymbolsFromASTRoots())->__invoke($sourcesASTs($additionalFiles)); + $definedVendorSymbols = array_unique(array_merge($vendorSymbols, $definedVendorSymbols)); + } $definedExtensionSymbols = (new LocateDefinedSymbolsFromExtensions())->__invoke( (new DefinedExtensionsResolver())->__invoke($composerJson, $options->getPhpCoreExtensions()) diff --git a/src/ComposerRequireChecker/DefinedSymbolsLocator/LocateDefinedSymbolsFromASTRoots.php b/src/ComposerRequireChecker/DefinedSymbolsLocator/LocateDefinedSymbolsFromASTRoots.php index 1e5fef11..561d02ff 100644 --- a/src/ComposerRequireChecker/DefinedSymbolsLocator/LocateDefinedSymbolsFromASTRoots.php +++ b/src/ComposerRequireChecker/DefinedSymbolsLocator/LocateDefinedSymbolsFromASTRoots.php @@ -2,7 +2,9 @@ namespace ComposerRequireChecker\DefinedSymbolsLocator; +use ArrayIterator; use ComposerRequireChecker\NodeVisitor\DefinedSymbolCollector; +use ComposerRequireChecker\NodeVisitor\IncludeCollector; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; use Traversable; @@ -21,15 +23,17 @@ public function __invoke(Traversable $ASTs): array $traverser->addVisitor(new NameResolver()); $traverser->addVisitor($collector = new DefinedSymbolCollector()); + $traverser->addVisitor($includes = new IncludeCollector()); $astSymbols = []; + $additionalFiles = []; foreach ($ASTs as $astRoot) { - $traverser->traverse($astRoot); - + $traverser->traverse($astRoot->getAst()); $astSymbols[] = $collector->getDefinedSymbols(); + $additionalFiles = array_merge($additionalFiles, $includes->getIncluded($astRoot->getFile())); } - return array_values(array_unique(array_merge([], ...$astSymbols))); + return [array_values(array_unique(array_merge([], ...$astSymbols))), new ArrayIterator($additionalFiles)]; } } diff --git a/src/ComposerRequireChecker/NodeVisitor/IncludeCollector.php b/src/ComposerRequireChecker/NodeVisitor/IncludeCollector.php new file mode 100644 index 00000000..b039150a --- /dev/null +++ b/src/ComposerRequireChecker/NodeVisitor/IncludeCollector.php @@ -0,0 +1,129 @@ +included = []; + return parent::beforeTraverse($nodes); + } + + /** + * @param string $file + * @return string[] + */ + public function getIncluded(string $file): array + { + $included = []; + foreach ($this->included as $exp) { + try { + $this->computePath($included, $this->processIncludePath($exp, $file), $file); + } catch(InvalidArgumentException $x) { + var_dump($x->getMessage()); + } + } + return $included; + } + + /** + * @param array $included + * @param string $path + * @param string $self + * @return void + */ + private function computePath(array &$included, string $path, string $self) + { + if (!preg_match('#^([A-Z]:)?/#i', str_replace('\\', '/', $path))) { + $path = dirname($self).'/'.$path; + } + if (false === strpos($path, '{var}')) { + $included[] = $path; + return; + } + $parts = explode('{var}', $path); + $regex = []; + foreach($parts as $part) { + $regex[] = preg_quote(str_replace('\\', '/', $part), '/'); + } + $regex = '/^'.implode('.+', $regex).'$/'; + $self = str_replace('\\', '/', $self); + foreach (new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $parts[0], + FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS + ) + ) as $file) { + $rfile = str_replace('\\', '/', $file); + if ($rfile !== $self && preg_match('/\\.php$/i', $rfile) && preg_match($regex, $rfile)) { + $included[] = $file; + } + } + } + + /** + * @param string|Exp $exp + * @param string $file + * @return string + * @throws InvalidArgumentException + */ + private function processIncludePath($exp, string $file): string + { + if (is_string($exp)) { + return $exp; + } + if ($exp instanceof Concat) { + return $this->processIncludePath($exp->left, $file).$this->processIncludePath($exp->right, $file); + } + if ($exp instanceof Dir) { + return dirname($file); + } + if ($exp instanceof File) { + return $file; + } + if ($exp instanceof ConstFetch && $exp->name === 'DIRECTORY_SEPARATOR') { + return DIRECTORY_SEPARATOR; + } + if ($exp instanceof String_) { + return $exp->value; + } + if ($exp instanceof Variable || $exp instanceof ConstFetch) { + return '{var}'; + } + throw new InvalidArgumentException('can\'t yet handle '.$exp->getType()); + } + + /** + * {@inheritDoc} + */ + public function enterNode(Node $node) + { + if ($node instanceof Include_) { + $this->included[] = $node->expr; + } + } +} diff --git a/src/ComposerRequireChecker/UsedSymbolsLocator/LocateUsedSymbolsFromASTRoots.php b/src/ComposerRequireChecker/UsedSymbolsLocator/LocateUsedSymbolsFromASTRoots.php index 5d72bd88..315c0685 100644 --- a/src/ComposerRequireChecker/UsedSymbolsLocator/LocateUsedSymbolsFromASTRoots.php +++ b/src/ComposerRequireChecker/UsedSymbolsLocator/LocateUsedSymbolsFromASTRoots.php @@ -25,7 +25,7 @@ public function __invoke(Traversable $ASTs): array $astSymbols = []; foreach ($ASTs as $astRoot) { - $traverser->traverse($astRoot); + $traverser->traverse($astRoot->getAst()); $astSymbols[] = $collector->getCollectedSymbols(); }