public function analyse(array $files, \Closure $progressCallback = null) : array
{
$errors = [];
if ($this->bootstrapFile !== null) {
if (!is_file($this->bootstrapFile)) {
return [sprintf('Bootstrap file %s does not exist.', $this->bootstrapFile)];
}
try {
require_once $this->bootstrapFile;
} catch (\Throwable $e) {
return [$e->getMessage()];
}
}
foreach ($this->ignoreErrors as $ignoreError) {
try {
\Nette\Utils\Strings::match('', $ignoreError);
} catch (\Nette\Utils\RegexpException $e) {
$errors[] = $e->getMessage();
}
}
if (count($errors) > 0) {
return $errors;
}
foreach ($files as $file) {
try {
if ($this->isExcludedFromAnalysing($file)) {
if ($progressCallback !== null) {
$progressCallback($file);
}
continue;
}
$fileErrors = [];
$this->nodeScopeResolver->processNodes($this->parser->parseFile($file), new Scope($this->broker, $this->printer, $file), function (\PhpParser\Node $node, Scope $scope) use(&$fileErrors) {
if ($node instanceof \PhpParser\Node\Stmt\Trait_) {
return;
}
$classes = array_merge([get_class($node)], class_parents($node));
foreach ($this->registry->getRules($classes) as $rule) {
$ruleErrors = $this->createErrors($node, $scope->getAnalysedContextFile(), $rule->processNode($node, $scope));
$fileErrors = array_merge($fileErrors, $ruleErrors);
}
});
if ($progressCallback !== null) {
$progressCallback($file);
}
$errors = array_merge($errors, $fileErrors);
} catch (\PhpParser\Error $e) {
$errors[] = new Error($e->getMessage(), $file);
} catch (\PHPStan\AnalysedCodeException $e) {
$errors[] = new Error($e->getMessage(), $file);
} catch (\Throwable $t) {
$errors[] = new Error(sprintf('Internal error: %s', $t->getMessage()), $file);
}
}
$unmatchedIgnoredErrors = $this->ignoreErrors;
$errors = array_values(array_filter($errors, function (string $error) use(&$unmatchedIgnoredErrors) : bool {
foreach ($this->ignoreErrors as $i => $ignore) {
if (\Nette\Utils\Strings::match($error, $ignore) !== null) {
unset($unmatchedIgnoredErrors[$i]);
return false;
}
}
return true;
}));
foreach ($unmatchedIgnoredErrors as $unmatchedIgnoredError) {
$errors[] = sprintf('Ignored error pattern %s was not matched in reported errors.', $unmatchedIgnoredError);
}
return $errors;
}
/** * @param string[] $paths * @param \Symfony\Component\Console\Style\StyleInterface $style * @param bool $defaultLevelUsed * @return int */ public function analyse(array $paths, StyleInterface $style, bool $defaultLevelUsed) : int { $errors = []; $files = []; $this->updateMemoryLimitFile(); foreach ($paths as $path) { $realpath = realpath($path); if ($realpath === false || !file_exists($realpath)) { $errors[] = new Error(sprintf('<error>Path %s does not exist</error>', $path), $path); } elseif (is_file($realpath)) { $files[] = $realpath; } else { $finder = new Finder(); foreach ($finder->files()->name('*.php')->in($realpath) as $fileInfo) { $files[] = $fileInfo->getPathname(); } } } $this->updateMemoryLimitFile(); $progressStarted = false; $fileOrder = 0; $errors = array_merge($errors, $this->analyser->analyse($files, function () use($style, &$progressStarted, $files, &$fileOrder) { if (!$progressStarted) { $style->progressStart(count($files)); $progressStarted = true; } $style->progressAdvance(); if ($fileOrder % 100 === 0) { $this->updateMemoryLimitFile(); } $fileOrder++; })); if ($progressStarted) { $style->progressFinish(); } if (count($errors) === 0) { $style->success('No errors'); if ($defaultLevelUsed) { $style->note(sprintf('PHPStan is performing only the most basic checks. You can pass a higher rule level through the --%s option (the default and current level is %d) to analyse code more thoroughly.', AnalyseCommand::OPTION_LEVEL, AnalyseCommand::DEFAULT_LEVEL)); } return 0; } $currentDir = realpath(dirname($paths[0])); $cropFilename = function ($filename) use($currentDir) { if ($currentDir !== false && strpos($filename, $currentDir) === 0) { return substr($filename, strlen($currentDir) + 1); } return $filename; }; $fileErrors = []; $notFileSpecificErrors = []; $totalErrorsCount = count($errors); foreach ($errors as $error) { if (is_string($error)) { $notFileSpecificErrors[] = [$error]; continue; } if (!isset($fileErrors[$error->getFile()])) { $fileErrors[$error->getFile()] = []; } $fileErrors[$error->getFile()][] = $error; } foreach ($fileErrors as $file => $errors) { $rows = []; foreach ($errors as $error) { $rows[] = [(string) $error->getLine(), $error->getMessage()]; } $style->table(['Line', $cropFilename($file)], $rows); } if (count($notFileSpecificErrors) > 0) { $style->table(['Error'], $notFileSpecificErrors); } $style->error(sprintf($totalErrorsCount === 1 ? 'Found %d error' : 'Found %d errors', $totalErrorsCount)); return 1; }