/** * @param int $category * The category of error such as `Issue::CATEGORY_UNDEFINED` * * @param string $type * The error type such as `Issue::UndeclaredMethod` * * @param int $severity * The severity of the issue such as `Issue::SEVERITY_NORMAL` * * @param string $message * The error message * * @param string $file * The name of the file with the issue * * @param int $lineno * The line number where the issue occurs */ public static function err(int $category, string $type, int $severity, string $message, string $file, int $lineno) { $log = self::getInstance(); // Don't report anything for excluded files if (Phan::isExcludedAnalysisFile($file)) { return; } // Don't report anything below our minimum severity // threshold if ($severity < Config::get()->minimum_severity) { return; } if ($category & $log->output_mask) { // This needs to be a sortable key so that output // is in the expected order $ukey = implode('|', [$file, str_pad((string) $lineno, 5, '0', STR_PAD_LEFT), $type, $message]); $log->msgs[$ukey] = ['file' => $file, 'lineno' => $lineno, 'category' => $category, 'type' => $type, 'severity' => $severity, 'message' => $message]; } }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzePropertyTypes(CodeBase $code_base, Clazz $clazz) { foreach ($clazz->getPropertyList($code_base) as $property) { try { $union_type = $property->getUnionType(); } catch (IssueException $exception) { Phan::getIssueCollector()->collectIssue($exception->getIssueInstance()); continue; } // Look at each type in the parameter's Union Type foreach ($union_type->getTypeSet() as $type) { // If its a native type or a reference to // self, its OK if ($type->isNativeType() || $type->isSelfType()) { continue; } // Otherwise, make sure the class exists $type_fqsen = $type->asFQSEN(); if (!$code_base->hasClassWithFQSEN($type_fqsen)) { Issue::emit(Issue::UndeclaredTypeProperty, $property->getFileRef()->getFile(), $property->getFileRef()->getLineNumberStart(), (string) $type_fqsen); } } } }
/** * This reads all files in `tests/files/src`, runs * the analyzer on each and compares the output * to the files's counterpart in * `tests/files/expected` * * @param string[] $test_file_list * @param string $expected_file_path * @dataProvider getTestFiles */ public function testFiles($test_file_list, $expected_file_path) { $expected_output = ''; if (is_file($expected_file_path)) { // Read the expected output $expected_output = trim(file_get_contents($expected_file_path)); } $stream = new BufferedOutput(); $printer = new PlainTextPrinter(); $printer->configureOutput($stream); Phan::setPrinter($printer); Phan::setIssueCollector(new BufferingCollector()); Phan::analyzeFileList($this->code_base, $test_file_list); $output = $stream->fetch(); // Uncomment to save the output back to the expected // output. This should be done for error message // text changes and only if you promise to be careful. /* $saved_output = $output; $test_file_elements= explode('/', $test_file_list[0]); $test_file_name = array_pop($test_file_elements); $saved_output = preg_replace('/[^ :\n]*\/' . $test_file_name . '/', '%s', $saved_output); $saved_output = preg_replace('/closure_[^\(]*\(/', 'closure_%s(', $saved_output); if (!empty($saved_output) && strlen($saved_output) > 0) { $saved_output .= "\n"; } file_put_contents($expected_file_path, $saved_output); $expected_output = trim(file_get_contents($expected_file_path)); */ $wanted_re = preg_replace('/\\r\\n/', "\n", $expected_output); // do preg_quote, but miss out any %r delimited sections $temp = ""; $r = "%r"; $startOffset = 0; $length = strlen($wanted_re); while ($startOffset < $length) { $start = strpos($wanted_re, $r, $startOffset); if ($start !== false) { // we have found a start tag $end = strpos($wanted_re, $r, $start + 2); if ($end === false) { // unbalanced tag, ignore it. $end = $start = $length; } } else { // no more %r sections $start = $end = $length; } // quote a non re portion of the string $temp = $temp . preg_quote(substr($wanted_re, $startOffset, $start - $startOffset), '/'); // add the re unquoted. if ($end > $start) { $temp = $temp . '(' . substr($wanted_re, $start + 2, $end - $start - 2) . ')'; } $startOffset = $end + 2; } $wanted_re = $temp; $wanted_re = str_replace(['%binary_string_optional%'], 'string', $wanted_re); $wanted_re = str_replace(['%unicode_string_optional%'], 'string', $wanted_re); $wanted_re = str_replace(['%unicode\\|string%', '%string\\|unicode%'], 'string', $wanted_re); $wanted_re = str_replace(['%u\\|b%', '%b\\|u%'], '', $wanted_re); // Stick to basics $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR, $wanted_re); $wanted_re = str_replace('%s', '[^\\r\\n]+', $wanted_re); $wanted_re = str_replace('%S', '[^\\r\\n]*', $wanted_re); $wanted_re = str_replace('%a', '.+', $wanted_re); $wanted_re = str_replace('%A', '.*', $wanted_re); $wanted_re = str_replace('%w', '\\s*', $wanted_re); $wanted_re = str_replace('%i', '[+-]?\\d+', $wanted_re); $wanted_re = str_replace('%d', '\\d+', $wanted_re); $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re); $wanted_re = str_replace('%f', '[+-]?\\.?\\d+\\.?\\d*(?:[Ee][+-]?\\d+)?', $wanted_re); $wanted_re = str_replace('%c', '.', $wanted_re); // %f allows two points "-.0.0" but that is the best *simple* expression $this->assertRegExp("/^{$wanted_re}\$/", $output, "Unexpected output in {$test_file_list[0]}"); }
/** * Visit a node with kind `\ast\AST_PROP` * * @param Node $node * A node of the type indicated by the method name that we'd * like to figure out the type that it produces. * * @return UnionType * The set of types that are possibly produced by the * given node */ public function visitProp(Node $node) : UnionType { try { $property = (new ContextNode($this->code_base, $this->context, $node))->getProperty($node->children['prop']); return $property->getUnionType(); } catch (IssueException $exception) { Phan::getIssueCollector()->collectIssue($exception->getIssueInstance()); } catch (CodeBaseException $exception) { $property_name = $node->children['prop']; Issue::emit(Issue::UndeclaredProperty, $this->context->getFile(), $node->lineno ?? 0, "{$exception->getFQSEN()}->{$property_name}"); } catch (UnanalyzableException $exception) { // Swallow it. There are some constructs that we // just can't figure out. } catch (NodeException $exception) { // Swallow it. There are some constructs that we // just can't figure out. } return new UnionType(); }
/** * This reads all files in `tests/files/src`, runs * the analyzer on each and compares the output * to the files's counterpart in * `tests/files/expected` * * @param string $test_file_path * @param string $expected_file_path * @dataProvider getTestFiles */ public function testFiles($test_file_path, $expected_file_path) { $expected_output = ''; if (is_file($expected_file_path)) { // Read the expected output $expected_output = trim(file_get_contents($expected_file_path)); } // Start reading everything sent to STDOUT // and compare it to the expected value once // the analzyer finishes running ob_start(); try { // Run the analyzer Phan::analyzeFileList($this->codeBase, [$test_file_path]); } catch (\Exception $exception) { // TODO: inexplicably bad things happen here // print "\n" . $exception->getMessage() . "\n"; } $output = trim(ob_get_clean()); // Uncomment to save the output back to the expected // output. This should be done for error message // text changes and only if you promise to be careful. /* $saved_output = $output; $saved_output = preg_replace('/[^ :\n]*\/'.$test_file_name.'/', '%s', $saved_output); $saved_output = preg_replace('/closure_[^\(]*\(/', 'closure_%s(', $saved_output); if (!empty($saved_output) && strlen($saved_output) > 0) { $saved_output .= "\n"; } file_put_contents($expected_file_path, $saved_output); $expected_output = trim(file_get_contents($expected_file_path)); */ $wanted_re = preg_replace('/\\r\\n/', "\n", $expected_output); // do preg_quote, but miss out any %r delimited sections $temp = ""; $r = "%r"; $startOffset = 0; $length = strlen($wanted_re); while ($startOffset < $length) { $start = strpos($wanted_re, $r, $startOffset); if ($start !== false) { // we have found a start tag $end = strpos($wanted_re, $r, $start + 2); if ($end === false) { // unbalanced tag, ignore it. $end = $start = $length; } } else { // no more %r sections $start = $end = $length; } // quote a non re portion of the string $temp = $temp . preg_quote(substr($wanted_re, $startOffset, $start - $startOffset), '/'); // add the re unquoted. if ($end > $start) { $temp = $temp . '(' . substr($wanted_re, $start + 2, $end - $start - 2) . ')'; } $startOffset = $end + 2; } $wanted_re = $temp; $wanted_re = str_replace(['%binary_string_optional%'], 'string', $wanted_re); $wanted_re = str_replace(['%unicode_string_optional%'], 'string', $wanted_re); $wanted_re = str_replace(['%unicode\\|string%', '%string\\|unicode%'], 'string', $wanted_re); $wanted_re = str_replace(['%u\\|b%', '%b\\|u%'], '', $wanted_re); // Stick to basics $wanted_re = str_replace('%e', '\\' . DIRECTORY_SEPARATOR, $wanted_re); $wanted_re = str_replace('%s', '[^\\r\\n]+', $wanted_re); $wanted_re = str_replace('%S', '[^\\r\\n]*', $wanted_re); $wanted_re = str_replace('%a', '.+', $wanted_re); $wanted_re = str_replace('%A', '.*', $wanted_re); $wanted_re = str_replace('%w', '\\s*', $wanted_re); $wanted_re = str_replace('%i', '[+-]?\\d+', $wanted_re); $wanted_re = str_replace('%d', '\\d+', $wanted_re); $wanted_re = str_replace('%x', '[0-9a-fA-F]+', $wanted_re); $wanted_re = str_replace('%f', '[+-]?\\.?\\d+\\.?\\d*(?:[Ee][+-]?\\d+)?', $wanted_re); $wanted_re = str_replace('%c', '.', $wanted_re); // %f allows two points "-.0.0" but that is the best *simple* expression $this->assertRegExp("/^{$wanted_re}\$/", $output, "Unexpected output in {$test_file_path}"); }
declare (strict_types=1); // Phan does a ton of GC and this offers a major speed // improvment if your system can handle it (which it // should be able to) gc_disable(); // Check the environment to make sure Phan can run successfully require_once __DIR__ . '/requirements.php'; // Build a code base based on PHP internally defined // functions, methods and classes before loading our // own $code_base = (require_once __DIR__ . '/codebase.php'); require_once __DIR__ . '/Phan/Bootstrap.php'; use Phan\CLI; use Phan\CodeBase; use Phan\Config; use Phan\Phan; // Create our CLI interface and load arguments $cli = new CLI(); $file_list = $cli->getFileList(); // If requested, expand the file list to a set of // all files that should be re-analyzed if (Config::get()->expand_file_list) { assert((bool) Config::get()->stored_state_file_path, 'Requesting an expanded dependency list can only ' . ' be done if a state-file is defined'); // Analyze the file list provided via the CLI $file_list = Phan::expandedFileList($code_base, $file_list); } // Analyze the file list provided via the CLI $is_issue_found = Phan::analyzeFileList($code_base, $file_list); // Provide an exit status code based on if // issues were found exit($is_issue_found ? EXIT_ISSUES_FOUND : EXIT_SUCCESS);
/** * @param IssueInstance $issue_instance * An issue instance to emit * * @return void */ public static function emitInstance(IssueInstance $issue_instance) { Phan::getIssueCollector()->collectIssue($issue_instance); }
/** * Create and read command line arguments, configuring * \Phan\Config as a side effect. */ public function __construct() { global $argv; // Parse command line args // still available: g,n,t,u,v,w $opts = getopt("f:m:o:c:k:aeqbr:pid:s:3:y:l:xj:zh::", ['backward-compatibility-checks', 'dead-code-detection', 'directory:', 'dump-ast', 'dump-signatures-file:', 'exclude-directory-list:', 'exclude-file:', 'file-list-only:', 'file-list:', 'help', 'ignore-undeclared', 'minimum-severity:', 'output-mode:', 'output:', 'parent-constructor-required:', 'progress-bar', 'project-root-directory:', 'quick', 'state-file:', 'processes:', 'config-file:', 'signature-compatibility', 'markdown-issue-messages']); // Determine the root directory of the project from which // we root all relative paths passed in as args Config::get()->setProjectRootDirectory($opts['d'] ?? $opts['project-root-directory'] ?? getcwd()); // Before reading the config, check for an override on // the location of the config file path. if (isset($opts['k'])) { $this->config_file = $opts['k']; } else { if (isset($opts['config-file'])) { $this->config_file = $opts['config-file']; } } // Now that we have a root directory, attempt to read a // configuration file `.phan/config.php` if it exists $this->maybeReadConfigFile(); $this->output = new ConsoleOutput(); $factory = new PrinterFactory(); $printer_type = 'text'; $minimum_severity = Config::get()->minimum_severity; $mask = -1; foreach ($opts ?? [] as $key => $value) { switch ($key) { case 'h': case 'help': $this->usage(); break; case 'r': case 'file-list-only': // Mark it so that we don't load files through // other mechanisms. $this->file_list_only = true; // Empty out the file list $this->file_list = []; // Intentionally fall through to load the // file list // Intentionally fall through to load the // file list case 'f': case 'file-list': $file_list = is_array($value) ? $value : [$value]; foreach ($file_list as $file_name) { $file_path = Config::projectPath($file_name); if (is_file($file_path) && is_readable($file_path)) { $this->file_list = array_merge($this->file_list, file(Config::projectPath($file_name), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)); } else { error_log("Unable to read file {$file_path}"); } } break; case 'l': case 'directory': if (!$this->file_list_only) { $directory_list = is_array($value) ? $value : [$value]; foreach ($directory_list as $directory_name) { $this->file_list = array_merge($this->file_list, $this->directoryNameToFileList($directory_name)); } } break; case 'k': case 'config-file': break; case 'm': case 'output-mode': if (!in_array($value, $factory->getTypes(), true)) { $this->usage(sprintf('Unknown output mode "%s". Known values are [%s]', $value, implode(',', $factory->getTypes()))); } $printer_type = $value; break; case 'c': case 'parent-constructor-required': Config::get()->parent_constructor_required = explode(',', $value); break; case 'q': case 'quick': Config::get()->quick_mode = true; break; case 'b': case 'backward-compatibility-checks': Config::get()->backward_compatibility_checks = true; break; case 'p': case 'progress-bar': Config::get()->progress_bar = true; break; case 'a': case 'dump-ast': Config::get()->dump_ast = true; break; case 'dump-signatures-file': Config::get()->dump_signatures_file = $value; break; case 'o': case 'output': $this->output = new StreamOutput(fopen($value, 'w')); break; case 'i': case 'ignore-undeclared': $mask ^= Issue::CATEGORY_UNDEFINED; break; case '3': case 'exclude-directory-list': Config::get()->exclude_analysis_directory_list = explode(',', $value); break; case 'exclude-file': Config::get()->exclude_file_list = array_merge(Config::get()->exclude_file_list, is_array($value) ? $value : [$value]); break; case 's': case 'state-file': // TODO: re-enable eventually // Config::get()->stored_state_file_path = $value; break; case 'j': case 'processes': Config::get()->processes = (int) $value; break; case 'z': case 'signature-compatibility': Config::get()->analyze_signature_compatibility = (bool) $value; break; case 'y': case 'minimum-severity': $minimum_severity = (int) $value; break; case 'd': case 'project-root-directory': // We handle this flag before parsing options so // that we can get the project root directory to // base other config flags values on break; case 'x': case 'dead-code-detection': Config::get()->dead_code_detection = true; break; case 'markdown-issue-messages': Config::get()->markdown_issue_messages = true; break; default: $this->usage("Unknown option '-{$key}'"); break; } } $printer = $factory->getPrinter($printer_type, $this->output); $filter = new ChainedIssueFilter([new FileIssueFilter(new Phan()), new MinimumSeverityFilter($minimum_severity), new CategoryIssueFilter($mask)]); $collector = new BufferingCollector($filter); Phan::setPrinter($printer); Phan::setIssueCollector($collector); $pruneargv = array(); foreach ($opts ?? [] as $opt => $value) { foreach ($argv as $key => $chunk) { $regex = '/^' . (isset($opt[1]) ? '--' : '-') . $opt . '/'; if (($chunk == $value || is_array($value) && in_array($chunk, $value)) && $argv[$key - 1][0] == '-' || preg_match($regex, $chunk)) { array_push($pruneargv, $key); } } } while ($key = array_pop($pruneargv)) { unset($argv[$key]); } foreach ($argv as $arg) { if ($arg[0] == '-') { $this->usage("Unknown option '{$arg}'"); } } if (!$this->file_list_only) { // Merge in any remaining args on the CLI $this->file_list = array_merge($this->file_list, array_slice($argv, 1)); // Merge in any files given in the config $this->file_list = array_merge($this->file_list, Config::get()->file_list); // Merge in any directories given in the config foreach (Config::get()->directory_list as $directory_name) { $this->file_list = array_merge($this->file_list, $this->directoryNameToFileList($directory_name)); } // Don't scan anything twice $this->file_list = array_unique($this->file_list); } // Exclude any files that should be excluded from // parsing and analysis (not read at all) if (count(Config::get()->exclude_file_list) > 0) { $exclude_file_set = []; foreach (Config::get()->exclude_file_list as $file) { $exclude_file_set[$file] = true; } $this->file_list = array_filter($this->file_list, function (string $file) use($exclude_file_set) : bool { return empty($exclude_file_set[$file]); }); } // We can't run dead code detection on multiple cores because // we need to update reference lists in a globally accessible // way during analysis. With our parallelization mechanism, there // is no shared state between processes, making it impossible to // have a complete set of reference lists. assert(Config::get()->processes === 1 || !Config::get()->dead_code_detection, "We cannot run dead code detection on more than one core."); }
/** * @param int $etype * The error type such as Log::EUNDEF. * * @param string $msg * The error message * * @param string $file * The name of the file with the issue * * @param int|null $lineno * The line number where the issue occurs */ public static function err(int $etype, string $msg, string $file = null, $lineno = 0) { $log = self::getInstance(); $lineno = (int) $lineno; if ($etype == self::EFATAL) { self::display(); // Something went wrong - abort if ($file) { throw new \Exception("{$file}:{$lineno} {$msg}"); } else { throw new \Exception($msg); } } // Don't report anything for excluded files if (Phan::isExcludedAnalysisFile($file)) { return; } // If configured to do so, prepend the message // with a trace ID which indicates where the issue // came from allowing us to group on unique classes // of issues if (Config::get()->emit_trace_id) { $msg = self::traceId(debug_backtrace()[1]) . ' ' . $msg; } if ($etype & $log->output_mask) { $ukey = md5($file . $lineno . $etype . $msg); $log->msgs[$ukey] = ['file' => $file, 'lineno' => $lineno, 'etype' => $etype, 'msg' => $msg]; } }
<?php declare (strict_types=1); // Check the environment to make sure Phan can run successfully require_once __DIR__ . '/requirements.php'; // Build a code base based on PHP internally defined // functions, methods and classes before loading our // own $code_base = (require_once __DIR__ . '/codebase.php'); require_once __DIR__ . '/Phan/Bootstrap.php'; use Phan\CLI; use Phan\CodeBase; use Phan\Config; use Phan\Phan; // Create our CLI interface and load arguments $cli = new CLI(); $file_list = $cli->getFileList(); // If requested, expand the file list to a set of // all files that should be re-analyzed if (Config::get()->expand_file_list) { assert((bool) Config::get()->stored_state_file_path, 'Requesting an expanded dependency list can only ' . ' be done if a state-file is defined'); // Analyze the file list provided via the CLI $file_list = Phan::expandedFileList($code_base, $file_list); } // Analyze the file list provided via the CLI Phan::analyzeFileList($code_base, $file_list);
<?php declare (strict_types=1); assert(extension_loaded('ast'), "The php-ast extension must be loaded in order for Phan to work. See https://github.com/etsy/phan#getting-it-running for more details."); assert((int) phpversion()[0] >= 7, "Phan requires PHP version 7 or greater. See https://github.com/etsy/phan#getting-it-running for more details."); // Grab these before we define our own classes $internal_class_name_list = get_declared_classes(); $internal_interface_name_list = get_declared_interfaces(); $internal_trait_name_list = get_declared_traits(); $internal_function_name_list = get_defined_functions()['internal']; require_once __DIR__ . '/Phan/Bootstrap.php'; use Phan\CLI; use Phan\CodeBase; use Phan\Config; use Phan\Phan; // Create our CLI interface and load arguments $cli = new CLI(); $code_base = new CodeBase($internal_class_name_list, $internal_interface_name_list, $internal_trait_name_list, $internal_function_name_list); // If requested, expand the file list to a set of // all files that should be re-analyzed if (Config::get()->expanded_dependency_list) { assert((bool) Config::get()->stored_state_file_path, 'Requesting an expanded dependency list can only ' . ' be done if a state-file is defined'); // Analyze the file list provided via the CLI $dependency_file_list = Phan::dependencyFileList($code_base, $cli->getFileList()); // Emit the expanded file list print implode("\n", $dependency_file_list) . "\n"; exit(1); } // Analyze the file list provided via the CLI Phan::analyzeFileList($code_base, $cli->getFileList());
/** * Create and read command line arguments, configuring * \Phan\Config as a side effect. */ public function __construct() { global $argv; // file_put_contents('/tmp/file', implode("\n", $argv)); // Parse command line args // still available: g,j,k,n,t,u,v,w,z $opts = getopt("f:m:o:c:aeqbr:pid:s:3:y:l:xh::", ['backward-compatibility-checks', 'dead-code-detection', 'directory:', 'dump-ast', 'exclude-directory-list:', 'expand-file-list', 'file-list-only:', 'file-list:', 'help', 'ignore-undeclared', 'minimum-severity:', 'output-mode:', 'output:', 'parent-constructor-required:', 'progress-bar', 'project-root-directory:', 'quick', 'state-file:']); // Determine the root directory of the project from which // we root all relative paths passed in as args Config::get()->setProjectRootDirectory($opts['d'] ?? getcwd()); // Now that we have a root directory, attempt to read a // configuration file `.phan/config.php` if it exists $this->maybeReadConfigFile(); $this->output = new ConsoleOutput(); $factory = new PrinterFactory(); $printer_type = 'text'; $minimum_severity = Config::get()->minimum_severity; $mask = -1; foreach ($opts ?? [] as $key => $value) { switch ($key) { case 'h': case 'help': $this->usage(); break; case 'r': case 'file-list-only': // Mark it so that we don't load files through // other mechanisms. $this->file_list_only = true; // Empty out the file list $this->file_list = []; // Intentionally fall through to load the // file list // Intentionally fall through to load the // file list case 'f': case 'file-list': $file_list = is_array($value) ? $value : [$value]; foreach ($file_list as $file_name) { if (is_file($file_name) && is_readable($file_name)) { $this->file_list = array_merge($this->file_list, file($file_name, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)); } else { error_log("Unable to read file {$file_name}"); } } break; case 'l': case 'directory': if (!$this->file_list_only) { $directory_list = is_array($value) ? $value : [$value]; foreach ($directory_list as $directory_name) { $this->file_list = array_merge($this->file_list, $this->directoryNameToFileList($directory_name)); } } break; case 'm': case 'output-mode': if (!in_array($value, $factory->getTypes(), true)) { $this->usage(sprintf('Unknown output mode "%s". Known values are [%s]', $value, implode(',', $factory->getTypes()))); } $printer_type = $value; break; case 'c': case 'parent-constructor-required': Config::get()->parent_constructor_required = explode(',', $value); break; case 'q': case 'quick': Config::get()->quick_mode = true; break; case 'b': case 'backward-compatibility-checks': Config::get()->backward_compatibility_checks = true; break; case 'p': case 'progress-bar': Config::get()->progress_bar = true; break; case 'a': case 'dump-ast': Config::get()->dump_ast = true; break; case 'e': case 'expand-file-list': Config::get()->expand_file_list = true; break; case 'o': case 'output': $this->output = new StreamOutput(fopen($value, 'w')); break; case 'i': case 'ignore-undeclared': $mask ^= Issue::CATEGORY_UNDEFINED; break; case '3': case 'exclude-directory-list': Config::get()->exclude_analysis_directory_list = explode(',', $value); break; case 's': case 'state-file': Config::get()->stored_state_file_path = $value; break; case 'y': case 'minimum-severity': $minimum_severity = (int) $value; break; case 'd': case 'project-root-directory': // We handle this flag before parsing options so // that we can get the project root directory to // base other config flags values on break; case 'x': case 'dead-code-detection': Config::get()->dead_code_detection = true; break; default: $this->usage("Unknown option '-{$key}'"); break; } } $printer = $factory->getPrinter($printer_type, $this->output); $filter = new ChainedIssueFilter([new FileIssueFilter(new Phan()), new MinimumSeverityFilter($minimum_severity), new CategoryIssueFilter($mask)]); $collector = new BufferingCollector($filter); Phan::setPrinter($printer); Phan::setIssueCollector($collector); $pruneargv = array(); foreach ($opts ?? [] as $opt => $value) { foreach ($argv as $key => $chunk) { $regex = '/^' . (isset($opt[1]) ? '--' : '-') . $opt . '/'; if (($chunk == $value || is_array($value) && in_array($chunk, $value)) && $argv[$key - 1][0] == '-' || preg_match($regex, $chunk)) { array_push($pruneargv, $key); } } } while ($key = array_pop($pruneargv)) { unset($argv[$key]); } foreach ($argv as $arg) { if ($arg[0] == '-') { $this->usage("Unknown option '{$arg}'"); } } if (!$this->file_list_only) { // Merge in any remaining args on the CLI $this->file_list = array_merge($this->file_list, array_slice($argv, 1)); // Merge in any files given in the config $this->file_list = array_merge($this->file_list, Config::get()->file_list); // Merge in any directories given in the config foreach (Config::get()->directory_list as $directory_name) { $this->file_list = array_merge($this->file_list, $this->directoryNameToFileList($directory_name)); } } }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitProp(Node $node) : Context { $property_name = $node->children['prop']; // Things like $foo->$bar if (!is_string($property_name)) { return $this->context; } assert(is_string($property_name), "Property must be string in context {$this->context}"); try { $class_list = (new ContextNode($this->code_base, $this->context, $node->children['expr']))->getClassList(); } catch (CodeBaseException $exception) { // This really shouldn't happen since the code // parsed cleanly. This should fatal. // throw $exception; return $this->context; } catch (\Exception $exception) { // If we can't figure out what kind of a class // this is, don't worry about it return $this->context; } foreach ($class_list as $clazz) { // Check to see if this class has the property or // a setter if (!$clazz->hasPropertyWithName($this->code_base, $property_name)) { if (!$clazz->hasMethodWithName($this->code_base, '__set')) { continue; } } try { $property = $clazz->getPropertyByNameInContext($this->code_base, $property_name, $this->context); } catch (IssueException $exception) { Phan::getIssueCollector()->collectIssue($exception->getIssueInstance()); return $this->context; } if (!$this->right_type->canCastToExpandedUnionType($property->getUnionType(), $this->code_base)) { Issue::emit(Issue::TypeMismatchProperty, $this->context->getFile(), $node->lineno ?? 0, (string) $this->right_type, "{$clazz->getFQSEN()}::{$property->getName()}", (string) $property->getUnionType()); return $this->context; } // After having checked it, add this type to it $property->getUnionType()->addUnionType($this->right_type); return $this->context; } if (Config::get()->allow_missing_properties) { try { // Create the property (new ContextNode($this->code_base, $this->context, $node))->getOrCreateProperty($property_name); } catch (\Exception $exception) { // swallow it } } elseif (!empty($class_list)) { Issue::emit(Issue::UndeclaredProperty, $this->context->getFile(), $node->lineno ?? 0, $property_name); } else { // If we hit this part, we couldn't figure out // the class, so we ignore the issue } return $this->context; }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitMethodCall(Node $node) : Context { $method_name = $node->children['method']; if (!is_string($method_name)) { return $this->context; } try { $method = (new ContextNode($this->code_base, $this->context, $node))->getMethod($method_name, false); } catch (IssueException $exception) { Phan::getIssueCollector()->collectIssue($exception->getIssueInstance()); return $this->context; } catch (NodeException $exception) { // If we can't figure out the class for this method // call, cry YOLO and mark every method with that // name with a reference. if (Config::get()->dead_code_detection && Config::get()->dead_code_detection_prefer_false_negative) { foreach ($this->code_base->getMethodListByName($method_name) as $method) { $method->addReference($this->context); } } // Swallow it return $this->context; } // Check the call for paraemter and argument types $this->analyzeCallToMethod($this->code_base, $method, $node); return $this->context; }
/** * Emit a log message if the type of the given * node cannot be cast to the given type * * @param Node|null|string|int $node * A node or whatever php-ast feels like returning * * @return bool * True if the cast is possible, else false */ private static function analyzeNodeUnionTypeCast($node, Context $context, CodeBase $code_base, UnionType $cast_type, \Closure $issue_instance) : bool { // Get the type of the node $node_type = UnionType::fromNode($context, $code_base, $node); // See if it can be cast to the given type $can_cast = $node_type->canCastToUnionType($cast_type); // If it can't, emit the log message if (!$can_cast) { Phan::getIssueCollector()->collectIssue($issue_instance($node_type)); } return $can_cast; }