/** * 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."); }
/** * 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)); } } }