/** * Get a Context after parsing the given * bit of code. */ private function contextForCode(string $code_stub) : Context { return Analysis::parseNodeInContext($this->code_base, new Context(), \ast\parse_code('<?php ' . $code_stub, Config::get()->ast_version)); }
/** * Analyze the given set of files and emit any issues * found to STDOUT. * * @param CodeBase $code_base * A code base needs to be passed in because we require * it to be initialized before any classes or files are * loaded. * * @param string[] $file_path_list * A list of files to scan * * @return null * We emit messages to STDOUT. Nothing is returned. * * @see \Phan\CodeBase */ public static function analyzeFileList(CodeBase $code_base, array $file_path_list) { $file_count = count($file_path_list); // We'll construct a set of files that we'll // want to run an analysis on $analyze_file_path_list = []; // This first pass parses code and populates the // global state we'll need for doing a second // analysis after. foreach ($file_path_list as $i => $file_path) { CLI::progress('parse', ($i + 1) / $file_count); // Kick out anything we read from the former version // of this file $code_base->flushDependenciesForFile($file_path); // If the file is gone, no need to continue if (!file_exists($file_path)) { continue; } try { // Parse the file Analysis::parseFile($code_base, $file_path); // Update the timestamp on when it was last // parsed $code_base->setParseUpToDateForFile($file_path); // Save this to the set of files to analyze $analyze_file_path_list[] = $file_path; } catch (\Throwable $throwable) { error_log($file_path . ' ' . $throwable->getMessage() . "\n"); } } // Don't continue on to analysis if the user has // chosen to just dump the AST if (Config::get()->dump_ast) { exit; } // Take a pass over all classes verifying various // states now that we have the whole state in // memory Analysis::analyzeClasses($code_base); // Take a pass over all functions verifying // various states now that we have the whole // state in memory Analysis::analyzeFunctions($code_base); // We can only save classes, methods, properties and // constants after we've merged parent classes in. $code_base->store(); // Once we know what the universe looks like we // can scan for more complicated issues. $file_count = count($analyze_file_path_list); foreach ($analyze_file_path_list as $i => $file_path) { CLI::progress('analyze', ($i + 1) / $file_count); // We skip anything defined as 3rd party code // to save a lil' time if (self::isExcludedAnalysisFile($file_path)) { continue; } // Analyze the file Analysis::analyzeFile($code_base, $file_path); } // Scan through all globally accessible elements // in the code base and emit errors for dead // code. Analysis::analyzeDeadCode($code_base); // Emit all log messages self::display(); }
/** * Analyze the given set of files and emit any issues * found to STDOUT. * * @param CodeBase $code_base * A code base needs to be passed in because we require * it to be initialized before any classes or files are * loaded. * * @param string[] $file_path_list * A list of files to scan * * @return bool * We emit messages to the configured printer and return * true if issues were found. * * @see \Phan\CodeBase */ public static function analyzeFileList(CodeBase $code_base, array $file_path_list) : bool { $file_count = count($file_path_list); // We'll construct a set of files that we'll // want to run an analysis on $analyze_file_path_list = []; // This first pass parses code and populates the // global state we'll need for doing a second // analysis after. foreach ($file_path_list as $i => $file_path) { CLI::progress('parse', ($i + 1) / $file_count); // Kick out anything we read from the former version // of this file $code_base->flushDependenciesForFile($file_path); // If the file is gone, no need to continue if (($real = realpath($file_path)) === false || !file_exists($real)) { continue; } try { // Parse the file Analysis::parseFile($code_base, $file_path); // Save this to the set of files to analyze $analyze_file_path_list[] = $file_path; } catch (\Throwable $throwable) { error_log($file_path . ' ' . $throwable->getMessage() . "\n"); } } // Don't continue on to analysis if the user has // chosen to just dump the AST if (Config::get()->dump_ast) { exit(EXIT_SUCCESS); } // With parsing complete, we need to tell the code base to // start hydrating any requested elements on their way out. // Hydration expands class types, imports parent methods, // properties, etc., and does stuff like that. // // This is an optimization that saves us a significant // amount of time on very large code bases. Instead of // hydrating all classes, we only hydrate the things we // actually need. When running as multiple processes this // lets us only need to do hydrate a subset of classes. $code_base->setShouldHydrateRequestedElements(true); // We used to scan all classes here, but now we do it // on demand after hydration. // Take a pass over all functions verifying // various states now that we have the whole // state in memory Analysis::analyzeFunctions($code_base); // Filter out any files that are to be excluded from // analysis $analyze_file_path_list = array_filter($analyze_file_path_list, function ($file_path) { return !self::isExcludedAnalysisFile($file_path); }); // Get the count of all files we're going to analyze $file_count = count($analyze_file_path_list); // Prevent an ugly failure if we have no files to // analyze. if (0 == $file_count) { return false; } // Get a map from process_id to the set of files that // the given process should analyze in a stable order $process_file_list_map = (new Ordering($code_base))->orderForProcessCount(Config::get()->processes, $analyze_file_path_list); // This worker takes a file and analyzes it $analysis_worker = function ($i, $file_path) use($file_count, $code_base) { CLI::progress('analyze', ($i + 1) / $file_count); Analysis::analyzeFile($code_base, $file_path); }; // Determine how many processes we're running on. This may be // less than the provided number if the files are bunched up // excessively. $process_count = count($process_file_list_map); assert($process_count > 0 && $process_count <= Config::get()->processes, "The process count must be between 1 and the given number of processes. After mapping files to cores, {$process_count} process were set to be used."); // Check to see if we're running as multiple processes // or not if ($process_count > 1) { // Run analysis one file at a time, splitting the set of // files up among a given number of child processes. $pool = new ForkPool($process_file_list_map, function () { // Remove any issues that were collected prior to forking // to prevent duplicate issues in the output. self::getIssueCollector()->reset(); }, $analysis_worker, function () { // Return the collected issues to be serialized. return self::getIssueCollector()->getCollectedIssues(); }); // Wait for all tasks to complete and collect the results. self::collectSerializedResults($pool->wait()); } else { // Get the task data from the 0th processor $analyze_file_path_list = array_values($process_file_list_map)[0]; // If we're not running as multiple processes, just iterate // over the file list and analyze them foreach ($analyze_file_path_list as $i => $file_path) { $analysis_worker($i, $file_path); } // Scan through all globally accessible elements // in the code base and emit errors for dead // code. Analysis::analyzeDeadCode($code_base); } // Get a count of the number of issues that were found $is_issue_found = 0 !== count(self::$issueCollector->getCollectedIssues()); // Collect all issues, blocking self::display(); return $is_issue_found; }
/** * For 'closed context' items (classes, methods, functions, * closures), we analyze children in the parent context, but * then return the parent context itself unmodified by the * children. * * │ * ▼ * ┌──●────┐ * │ │ * ●──●──● │ * ┌────┘ * ● * │ * ▼ * * @param Node $node * An AST node we'd like to determine the UnionType * for * * @return Context * The updated context after visiting the node */ public function visitClosedContext(Node $node) : Context { // Make a copy of the internal context so that we don't // leak any changes within the closed context to the // outer scope $context = clone $this->context->withLineNumberStart($node->lineno ?? 0); // Visit the given node populating the code base // with anything we learn and get a new context // indicating the state of the world within the // given node $context = (new PreOrderAnalysisVisitor($this->code_base, $context))($node); assert(!empty($context), 'Context cannot be null'); // We collect all child context so that the // PostOrderAnalysisVisitor can optionally operate on // them $child_context_list = []; $child_context = $context; // With a context that is inside of the node passed // to this method, we analyze all children of the // node. foreach ($node->children ?? [] as $child_node) { // Skip any non Node children. if (!$child_node instanceof Node) { continue; } if (!Analysis::shouldVisit($child_node)) { $child_context->withLineNumberStart($child_node->lineno ?? 0); continue; } // Step into each child node and get an // updated context for the node $child_context = (new BlockAnalysisVisitor($this->code_base, $child_context, $node, $this->depth + 1))($child_node); $child_context_list[] = $child_context; } // For if statements, we need to merge the contexts // of all child context into a single scope based // on any possible branching structure $context = (new ContextMergeVisitor($this->code_base, $context, $child_context_list))($node); // Now that we know all about our context (like what // 'self' means), we can analyze statements like // assignments and method calls. $context = (new PostOrderAnalysisVisitor($this->code_base, $context->withLineNumberStart($node->lineno ?? 0), $this->parent_node))($node); // Return the initial context as we exit return $this->context; }
/** * @return null * Analyze the node associated with this object * in the given context */ public function analyze(Context $context, CodeBase $code_base) : Context { // Don't do anything if we care about being // fast if (Config::get()->quick_mode) { return $context; } if (!$this->hasNode()) { return $context; } // Closures depend on the context surrounding them such // as for getting `use(...)` variables. Since we don't // have them, we can't re-analyze them until we change // that. // // TODO: Store the parent context on Analyzable objects if ($this->getNode()->kind === \ast\AST_CLOSURE) { return $context; } // Don't go deeper than one level in if ($this->recursion_depth++ > 2) { return $context; } // Analyze the node in a cloned context so that we // don't overwrite anything $context = Analysis::analyzeNodeInContext($code_base, clone $context, $this->getNode()); return $context; }
/** * Analyze the given set of files and emit any issues * found to STDOUT. * * @param CodeBase $code_base * A code base needs to be passed in because we require * it to be initialized before any classes or files are * loaded. * * @param string[] $file_path_list * A list of files to scan * * @return null * We emit messages to STDOUT. Nothing is returned. * * @see \Phan\CodeBase */ public static function analyzeFileList(CodeBase $code_base, array $file_path_list) { $file_count = count($file_path_list); // We'll construct a set of files that we'll // want to run an analysis on $analyze_file_path_list = []; // This first pass parses code and populates the // global state we'll need for doing a second // analysis after. foreach ($file_path_list as $i => $file_path) { CLI::progress('parse', ($i + 1) / $file_count); // Kick out anything we read from the former version // of this file $code_base->flushDependenciesForFile($file_path); // If the file is gone, no need to continue if (!file_exists($file_path)) { continue; } try { // Parse the file Analysis::parseFile($code_base, $file_path); // Save this to the set of files to analyze $analyze_file_path_list[] = $file_path; } catch (\Throwable $throwable) { error_log($file_path . ' ' . $throwable->getMessage() . "\n"); } } // Don't continue on to analysis if the user has // chosen to just dump the AST if (Config::get()->dump_ast) { exit(EXIT_SUCCESS); } // With parsing complete, we need to tell the code base to // start hydrating any requested elements on their way out. // Hydration expands class types, imports parent methods, // properties, etc., and does stuff like that. // // This is an optimization that saves us a significant // amount of time on very large code bases. Instead of // hydrating all classes, we only hydrate the things we // actually need. When running as multiple processes this // lets us only need to do hydrate a subset of classes. $code_base->setShouldHydrateRequestedElements(true); // Take a pass over all classes verifying various // states now that we have the whole state in // memory Analysis::analyzeClasses($code_base); // Take a pass over all functions verifying // various states now that we have the whole // state in memory Analysis::analyzeFunctions($code_base); // We can only save classes, methods, properties and // constants after we've merged parent classes in. // TODO: Reinstate this // $code_base->store(); // Filter out any files that are to be excluded from // analysis $analyze_file_path_list = array_filter($analyze_file_path_list, function ($file_path) { return !self::isExcludedAnalysisFile($file_path); }); // Get the count of all files we're going to analyze $file_count = count($analyze_file_path_list); // This worker takes a file and analyzes it $analysis_worker = function ($i, $file_path) use($file_count, $code_base) { CLI::progress('analyze', ($i + 1) / $file_count); Analysis::analyzeFile($code_base, $file_path); }; // Check to see if we're running as multiple processes // or not if (Config::get()->processes > 1) { // Collect all issues, blocking self::display(); // Run analysis one file at a time, splitting the set of // files up among a given number of child processes. $pool = new ForkPool(Config::get()->processes, $analyze_file_path_list, function () { }, $analysis_worker, function () { self::display(); }); // Wait for all tasks to complete $pool->wait(); } else { // If we're not running as multiple processes, just iterate // over the file list and analyze them foreach ($analyze_file_path_list as $i => $file_path) { $analysis_worker($i, $file_path); } // Scan through all globally accessible elements // in the code base and emit errors for dead // code. Analysis::analyzeDeadCode($code_base); // Collect all issues, blocking self::display(); } }