public static function addSourcesToProject(InputInterface $input, Project $project) { $inputSources = []; if (NULL !== $input->getOption('load-from')) { $inputSources = array_merge($inputSources, array_map('trim', file($input->getOption('load-from')))); } if (NULL !== $input->getArgument('sources')) { $inputSources = array_merge($inputSources, $input->getArgument('sources')); } $inputSources = array_unique($inputSources); if (empty($inputSources)) { throw new \RuntimeException('No input sources/directories for scanning provided.'); } /** @var \SplFileInfo[] $files */ $files = []; foreach ($inputSources as $inputSource) { if (is_dir($inputSource)) { $files = array_merge($files, Util::scanDir($inputSource)); } else { if (is_file($inputSource)) { $files[] = new \SplFileInfo($inputSource); } else { throw new \RuntimeException("File {$inputSource} is not a file nor a directory."); } } } if (count($files) === 0) { throw new \RuntimeException('No source files to scan found'); } foreach ($files as $file) { $project->addSplFileInfo($file); } }
public function main() { if (NULL === $this->filesets) { throw new \BuildException('No fileset provided'); } $project = new Project(new Phing($this)); $project->addListener($listener = new PhingListener($this)); $analyzers = NULL !== $this->getConfigFile() ? require $this->getConfigFile() : Project::getDefaultConfig(); $project->addAnalyzers($analyzers); # Add files foreach ($this->filesets as $fs) { $ds = $fs->getDirectoryScanner($this->project); /** @var \PhingFile $fromDir */ $fromDir = $fs->getDir($this->project); /** @var $files */ $files = $ds->getIncludedFiles(); foreach ($files as $file) { $fileName = $fromDir->getAbsolutePath() . DIRECTORY_SEPARATOR . $file; $this->log('Adding file ' . $fileName, \Project::MSG_VERBOSE); $project->addSplFileInfo(new \SplFileInfo($fileName)); } } $project->analyze(); $buildErrorMessage = $listener->getBuildErrorMessage(); if ($this->haltonerror && !empty($buildErrorMessage)) { throw new \BuildException($buildErrorMessage); } }
/** * @param Project $project */ public function analyze(Project $project) { $traverser = new NodeTraverser(); $traverser->addVisitor(new PhpParserNameResolver()); foreach ($project->getFiles() as $file) { $traverser->traverse($file->getTree()); } }
public function projectEnd(Project $project) { if ($this->logfileWriter && 0 === strpos($this->logFormat, 'json')) { $options = $this->logFormat === 'json-pretty' ? JSON_PRETTY_PRINT : 0; $formatter = new Json($options); /** @noinspection PhpParamsInspection */ $this->logfileWriter->write($formatter->formatReports($project->getAnalyzerReports())); } }
/** * @param Project $project */ public function analyze(Project $project) { foreach ($this->graph->getObjects() as $object) { if ($object instanceof ParsedInterface) { foreach ($object->getMethods() as $method) { if ($method->getMethod()->isAbstract()) { $report = new StringReport('Access type for interface method ' . $object->getName() . '::' . $method->getNameAndParamsSignature() . ' must be ommmited in ' . $object->getFile()->getSplFile()->getRealPath() . ':' . $object->getInterface()->getLine()); $report->setSourceFragment(new SourceFragment($object->getFile(), new Lines($method->getMethod()->getAttribute('startLine') - 1 - $this->sourceContext, $method->getMethod()->getAttribute('endLine') - 1 + $this->sourceContext, $method->getMethod()->getAttribute('startLine') - 1))); $project->addReport($report); } } } } }
/** * @param Project $project */ public function analyze(Project $project) { foreach ($this->graph->getObjects() as $object) { if ($object instanceof ParsedInterface) { foreach ($object->getMethods() as $method) { $methodCompare = new MethodSignatureCompare($method); foreach ($this->checkInterfaceMethods($methodCompare, $this->helper->findInterfaceImplements($object)) as $brokenInterfaceAndMethod) { $report = new Report($brokenInterfaceAndMethod, $method); $report->setSourceFragment(new SourceFragment($object->getFile(), new Lines($method->getMethod()->getAttribute('startLine') - 1 - $this->sourceContext, $method->getMethod()->getAttribute('endLine') - 1 + $this->sourceContext, $method->getMethod()->getAttribute('startLine') - 1))); $project->addReport($report); } } } } }
protected function execute(InputInterface $input, OutputInterface $output) { $logger = new SymfonyConsoleOutput($output); $project = new Project($logger); if ($input->getOption('quiet')) { $logger->setReportingLevel(Logger::ERROR); } else { if ($input->getOption('verbose')) { $logger->setReportingLevel(Logger::INFO); } } # Fall back to UTC if date.timezone is not set $dateTimezone = ini_get('date.timezone'); if (empty($dateTimezone)) { $logger->warning('date.timezone not set, falling back to UTC'); date_default_timezone_set('UTC'); } SourceHandler::addSourcesToProject($input, $project); $analyzers = NULL !== $input->getOption('config') ? require $input->getOption('config') : Project::getDefaultConfig(); # Configure listener $listener = NULL; if (NULL !== $input->getOption('report')) { $format = NULL === $input->getOption('format') ? 'plain' : $input->getOption('format'); if ('plain' === $format) { $listener = new Plain(fopen($input->getOption('report'), 'w')); } else { if (0 === strpos($format, 'json')) { $options = 0; if ('json-pretty' === $format) { $options = JSON_PRETTY_PRINT; } $json = new JsonFormatter($options); $listener = new Json(fopen($input->getOption('report'), 'w'), $json); } else { throw new \RuntimeException("Unsupported report format '{$format}'"); } } } else { if (!$input->getOption('quiet')) { $listener = new Plain(\STDOUT); } } if (NULL !== $listener) { $project->addListener($listener); } $project->addAnalyzers($analyzers); $project->analyze(); $analyzerReports = $project->getAnalyzerReports(); if (count($analyzerReports) > 0) { return 1; } else { $output->writeln('Nothing found to report.'); return 0; } }
/** * Called when entering a node. * * Return value semantics: * * null: $node stays as-is * * otherwise: $node is set to the return value * * @param Node $node Node * * @return null|Node Node */ public function enterNode(Node $node) { if ($node instanceof Node\Stmt\Catch_) { $this->subNodeTraverser->traverse($node->stmts); $nonComments = $this->nonCommentCounterVisitor->getNonCommentStatements(); if (0 === $nonComments) { $report = new StringReport('Empty catch block found'); $line = $node->getAttribute('startLine') - 1; $report->setSourceFragment(new SourceFragment($this->currentFile, new Lines($line - $this->sourceContext, $line + $this->sourceContext, $line))); $this->project->addReport($report); } } }
/** * Called when entering a node. * * Return value semantics: * * null: $node stays as-is * * otherwise: $node is set to the return value * * @param Node $node Node * * @return null|Node Node */ public function enterNode(Node $node) { if ($node instanceof New_) { if ($node->class instanceof Variable) { $msg = 'Dynamic class instantiation with variable '; if ($node->class->name instanceof Variable) { $msg .= 'variable $' . $node->class->name->name; } else { $msg .= '$' . $node->class->name; } $report = new StringReport($msg . ' in ' . $this->currentFile->getSplFile()->getFilename() . ':' . $node->class->getLine()); $report->setSourceFragment(new SourceFragment($this->currentFile, new Lines($node->class->getAttribute('startLine') - 1 - $this->sourceContext, $node->class->getAttribute('endLine') - 1 + $this->sourceContext, $node->class->getAttribute('startLine') - 1))); $this->project->addReport($report); } } }
/** * @param Project $project */ public function analyze(Project $project) { $this->project = $project; $traverser = new NodeTraverser(); $traverser->addVisitor($this); foreach ($project->getFiles() as $file) { $this->currentFile = $file; $traverser->traverse($file->getTree()); foreach ($this->variables as $variable) { $report = new StringReport('Variable used in constructing raw SQL, is it escaped?'); $line = $variable->getAttribute('startLine') - 1; $report->setSourceFragment(new SourceFragment($file, new Lines($line - $this->sourceContext, $line + $this->sourceContext, $line))); $project->addReport($report); } } }
/** * @param Project $project * @return \string[] */ public function analyze(Project $project) { $graph = $this->objectGraph; /** @var ParsedClass[] $classesMissingMethod */ $classesMissingMethods = new Map(function ($key) { if ($key instanceof ParsedClass) { return; } throw new MapException('Only keys of type Class_ are accepted'); }, function ($value) { if (is_array($value)) { return; } throw new MapException('Only values of type array are accepted'); }); # scan all objects (we're actually only interested in classes) foreach ($graph->getObjects() as $object) { if ($object instanceof ParsedClass) { foreach ($object->getMethods() as $method) { # and see if they've abstract methods if ($method->getMethod()->isAbstract()) { $methodName = $method->getNormalizedName(); # now find all descendant classes and see if they've implemented it $classesMissingMethod = $this->findSubtypeUntilMethodMatchesRecursive($methodName, $this->helper->findExtends($object)); # in case we found ones, store them for reporting later # note: we may find other methods in the same class later too foreach ($classesMissingMethod as $classMissingMethod) { $methods = []; if ($classesMissingMethods->exists($classMissingMethod)) { $methods = $classesMissingMethods->get($classMissingMethod); } $methods[] = $method; $classesMissingMethods->set($classMissingMethod, $methods); } } } } } /** @var ParsedClass $class */ foreach ($classesMissingMethods->keys() as $class) { $project->addReport(new AbstractMissingReport($class, $classesMissingMethods->get($class))); } }
public function enterNode(Node $node) { if ($node instanceof Namespace_ && NULL !== $node->name) { $this->currentNamespace = join('\\', $node->name->parts); } else { if ($node instanceof Use_ && $node->type === Use_::TYPE_NORMAL) { $this->currentUseStatements[] = $node; } else { if ($node instanceof PhpParserClass || $node instanceof PhpParserInterface) { /** @var ParsedObject|NULL $object */ $object = NULL; if ($node instanceof PhpParserClass) { $object = new ParsedClass($this->currentNamespace, $this->currentUseStatements, $node, $this->currentFile); } else { if ($node instanceof PhpParserInterface) { $object = new ParsedInterface($this->currentNamespace, $this->currentUseStatements, $node, $this->currentFile); } } if (NULL !== $object) { try { $this->addObject($object); } catch (ObjectAlreadyExistsException $e) { $existingObject = $this->getObjectByFqn($object->getName()); $msg = 'Multiple declarations of the same type are not supported. Symbol ' . $object->getName() . ' from ' . $this->currentFile->getSplFile()->getRealPath() . ':' . $node->getLine(); if ($existingObject instanceof ParsedObject) { $msg .= ' already found in ' . $existingObject->getFile()->getSplFile()->getRealPath() . ':' . $existingObject->getNode()->getLine() . ' ; only the first ' . 'encounter is used'; } else { if ($existingObject instanceof ReflectedObject) { $msg .= ' clashes with internal ' . strtolower($existingObject->getKind()) . ' ' . $existingObject->getName(); } else { throw new \RuntimeException('Unknown existing object ' . $existingObject->getName()); } } $this->project->getLogger()->warning($msg); } } } } } }
/** * @param Project $project * @return \string[] */ public function analyze(Project $project) { $graph = $this->objectGraph; /** @var ParsedClass[] $classesMissingMethod */ $classesMissingMethods = new Map(function ($key) { if ($key instanceof ParsedClass) { return; } throw new MapException('Only keys of type Class_ are accepted'); }, function ($value) { if (is_array($value)) { return; } throw new MapException('Only values of type array are accepted'); }); # scan all objects (we're actually only interested in interfaces) foreach ($graph->getObjects() as $object) { if ($object instanceof ParsedInterface) { foreach ($object->getMethods() as $method) { # now find all classes and class from interfaces extending this # interface $classesMissingMethod = $this->findSubtypeUntilMethodMatchesRecursive($method, $this->helper->findImplements($object)); # in case we found ones, store them for reporting later # note: we may find other methods in the same class later too foreach ($classesMissingMethod as $classMissingMethod) { $methods = []; if ($classesMissingMethods->exists($classMissingMethod)) { $methods = $classesMissingMethods->get($classMissingMethod); } $methods[] = $method; $classesMissingMethods->set($classMissingMethod, $methods); } } } } /** @var ParsedClass $class */ foreach ($classesMissingMethods->keys() as $class) { $project->addReport(new InterfaceMissingReport($class, $classesMissingMethods->get($class))); } }
/** * @param Project $project */ public function analyze(Project $project) { $logger = $project->getLogger(); foreach ($project->getSplFileInfos() as $splFileInfo) { try { $source = file($splFileInfo->getRealPath()); $code = join('', $source); } catch (\RuntimeException $e) { $project->addReport(new StringReport($e->getMessage())); continue; } try { $logger->info('Parsing ' . $splFileInfo->getRealPath()); $tree = $this->parser->parse($code); $project->addFile(new File($splFileInfo, $source, $tree)); } catch (Error $e) { $project->getLogger()->warning('[' . $this->getName() . '] ' . 'Error while parsing ' . $splFileInfo->getRealPath() . ' : ' . $e->getMessage()); $project->addReport(new FileParserErrorReport($splFileInfo, $e)); } } }
/** * Although this test seems redundant I use it to ensure that as far as it's * possible the analyzers do not negatively affect each other. */ public function testAllAnalyzers() { $project = new Project(Null::getInstance()); foreach (Util::scanDir(self::getAnalyzerTestsDir(), '/\\.phptest$/') as $file) { $project->addSplFileInfo(new \SplFileInfo($file)); } $project->addAnalyzers(Project::getDefaultConfig()); $project->analyze(); $reports = $project->getAnalyzerReports(); $this->assertSame(10, count($reports)); $this->assertSame('Class Mfn\\PHP\\Analyzer\\Tests\\AbstractMethodMissing\\b misses the following abstract method: Mfn\\PHP\\Analyzer\\Tests\\AbstractMethodMissing\\a::b()', $reports[0]->getTimestampedReport()->getReport()->report()); $this->assertSame('Class Mfn\\PHP\\Analyzer\\Tests\\InterfaceMethodMissing\\d misses the following interface method: Mfn\\PHP\\Analyzer\\Tests\\InterfaceMethodMissing\\a::c()', $reports[1]->getTimestampedReport()->getReport()->report()); $this->assertSame('Declaration of Mfn\\PHP\\Analyzer\\Tests\\MethodDeclarationCompatibility\\b::c($a, $a) must be compatible with Mfn\\PHP\\Analyzer\\Tests\\MethodDeclarationCompatibility\\a::c(array $a = 1)', $reports[2]->getTimestampedReport()->getReport()->report()); # Empty exception catch block reports $this->assertSame(27, $reports[3]->getSourceFragment()->getLineSegment()->getHighlightLine()); $this->assertSame('Dynamic class instantiation with variable $foo in 003_dynamic_class_instantiation.phptest:26', $reports[4]->getTimestampedReport()->getReport()->report()); $this->assertSame('Variable used in constructing raw SQL, is it escaped?', $reports[5]->getTimestampedReport()->getReport()->report()); $this->assertSame(29, $reports[5]->getTimestampedReport()->getReport()->getSourceFragment()->getLineSegment()->getHighlightLine()); $this->assertSame(30, $reports[6]->getTimestampedReport()->getReport()->getSourceFragment()->getLineSegment()->getHighlightLine()); $this->assertSame(31, $reports[7]->getTimestampedReport()->getReport()->getSourceFragment()->getLineSegment()->getHighlightLine()); # Empty exception catch block reports $this->assertSame(27, $reports[8]->getSourceFragment()->getLineSegment()->getHighlightLine()); $this->assertSame(32, $reports[9]->getSourceFragment()->getLineSegment()->getHighlightLine()); }
* Automatically generate analyzers markdown doc from classes */ use Mfn\PHP\Analyzer\Analyzers\NameResolver; use Mfn\PHP\Analyzer\Analyzers\ObjectGraph\Helper; use Mfn\PHP\Analyzer\Analyzers\ObjectGraph\ObjectGraph; use Mfn\PHP\Analyzer\Analyzers\Parser; use Mfn\PHP\Analyzer\Logger\Stdout; use Mfn\PHP\Analyzer\Project; use Mfn\PHP\Analyzer\Util\Util; use PhpParser\Lexer; require_once __DIR__ . '/../vendor/autoload.php'; error_reporting(E_ALL); Util::installMinimalError2ExceptionHandler(); $projectRealPath = realpath(__DIR__ . DIRECTORY_SEPARATOR . '..'); # Use analyzer to gather the object graph $project = new Project(new Stdout()); $project->addSplFileInfos(Util::scanDir(__DIR__ . '/../lib/')); $project->addAnalyzers([new Parser(new \PhpParser\Parser(new Lexer())), new NameResolver(), $objectGraph = new ObjectGraph()]); $project->analyze(); $helper = new Helper($objectGraph); $className = 'Mfn\\PHP\\Analyzer\\Analyzers\\Analyzer'; $class = $objectGraph->getClassByFqn($className); if (NULL === $class) { throw new \RuntimeException("Unable to find class {$className}"); } unset($className); $descendants = $helper->findExtends($class, true); sort($descendants); /** @var string[] $index */ $index = []; $index[] = '# Built-in / available Analyzers';
public function projectEnd(Project $project) { $this->write($this->json->formatReports($project->getAnalyzerReports())); }