*/ private $new_sinks = []; /** * @var array */ private $new_sources = []; /** * @var array */ private static $previous_sinks = []; /** * @var array */ private static $previous_sources = []; /** * @var array */ private static $archived_sinks = []; /** * @var array */ private static $archived_sources = []; /** * @var array> */ private $specializations = []; public function __construct() { self::$previous_sinks = []; self::$previous_sources = []; self::$archived_sinks = []; self::$archived_sources = []; } public function hasExistingSink(Taintable $sink) : ?Sink { return self::$archived_sinks[$sink->id] ?? null; } public function hasExistingSource(Taintable $source) : ?Source { return self::$archived_sources[$source->id] ?? null; } public function hasNewOrExistingSink(Taintable $sink) : ?Sink { return $this->new_sinks[$sink->id] ?? self::$archived_sinks[$sink->id] ?? null; } public function hasNewOrExistingSource(Taintable $source) : ?Source { return $this->new_sources[$source->id] ?? self::$archived_sources[$source->id] ?? null; } /** * @param ?array $suffixes */ public function hasPreviousSink(Sink $source, ?array &$suffixes = null) : ?Sink { if (isset($this->specializations[$source->id])) { $suffixes = $this->specializations[$source->id]; foreach ($suffixes as $suffix) { if (isset(self::$previous_sinks[$source->id . '-' . $suffix])) { return self::$previous_sinks[$source->id . '-' . $suffix]; } } return null; } return self::$previous_sinks[$source->id] ?? null; } /** * @param ?array $suffixes */ public function hasPreviousSource(Source $source, ?array &$suffixes = null) : ?Source { if (isset($this->specializations[$source->id])) { $suffixes = $this->specializations[$source->id]; foreach ($suffixes as $suffix) { if (isset(self::$previous_sources[$source->id . '-' . $suffix])) { return self::$previous_sources[$source->id . '-' . $suffix]; } } return null; } return self::$previous_sources[$source->id] ?? null; } public function addSpecialization(string $base_id, string $suffix) : void { if (isset($this->specializations[$base_id])) { if (!\in_array($suffix, $this->specializations)) { $this->specializations[$base_id][] = $suffix; } } else { $this->specializations[$base_id] = [$suffix]; } } /** * @param array $sources */ public function addSources( array $sources ) : void { foreach ($sources as $source) { if ($this->hasExistingSource($source)) { continue; } if ($this->hasExistingSink($source) && $source->code_location) { // do nothing } $this->new_sources[$source->id] = $source; } } /** * @param array $sinks */ public function addSinks( array $sinks ) : void { foreach ($sinks as $sink) { if ($this->hasExistingSink($sink)) { continue; } if ($this->hasExistingSource($sink) && $sink->code_location) { // do nothing } $this->new_sinks[$sink->id] = $sink; } } /** * @var array $visited_paths */ public function getPredecessorPath(Source $source, array $visited_paths = []) : string { $location_summary = ''; if ($source->code_location) { $location_summary = $source->code_location->getQuickSummary(); } if (isset($visited_paths[$source->id . ' ' . $location_summary])) { return ''; } $visited_paths[$source->id . ' ' . $location_summary] = true; $source_descriptor = $source->id . ($location_summary ? ' (' . $location_summary . ')' : ''); $previous_source = $source->parents[0] ?? null; if ($previous_source) { if ($previous_source === $source) { return ''; } return $this->getPredecessorPath($previous_source, $visited_paths) . ' -> ' . $source_descriptor; } return $source_descriptor; } /** * @var array $visited_paths */ public function getSuccessorPath(Sink $sink, array $visited_paths = []) : string { $location_summary = ''; if ($sink->code_location) { $location_summary = $sink->code_location->getQuickSummary(); } if (isset($visited_paths[$sink->id . ' ' . $location_summary])) { return ''; } $visited_paths[$sink->id . ' ' . $location_summary] = true; $sink_descriptor = $sink->id . ($location_summary ? ' (' . $location_summary . ')' : ''); $next_sink = $sink->children[0] ?? null; if ($next_sink) { if ($next_sink === $sink) { return ''; } return $sink_descriptor . ' -> ' . $this->getSuccessorPath($next_sink, $visited_paths); } return $sink_descriptor; } public function hasNewSinksAndSources() : bool { foreach ($this->new_sinks as $sink) { if ($sink && ($existing_source = $this->hasNewOrExistingSource($sink)) && $sink->code_location) { $last_location = $sink; while ($last_location->children) { $first_child = \reset($last_location->children); if (!$first_child->code_location) { break; } $last_location = $first_child; } if (IssueBuffer::accepts( new TaintedInput( 'in path ' . $this->getPredecessorPath($existing_source) . ' out path ' . $this->getSuccessorPath($sink), $last_location->code_location ?: $sink->code_location ) )) { // fall through } } } foreach ($this->new_sources as $source) { if ($source && ($existing_sink = $this->hasNewOrExistingSink($source)) && $source->code_location) { $last_location = $existing_sink; while ($last_location->children) { $first_child = \reset($last_location->children); if (!$first_child->code_location) { break; } $last_location = $first_child; } if (IssueBuffer::accepts( new TaintedInput( 'in path ' . $this->getPredecessorPath($source) . ' out path ' . $this->getSuccessorPath($existing_sink), $last_location->code_location ?: $source->code_location ) )) { // fall through } } } if (!self::$archived_sources && !$this->new_sources) { return false; } return $this->new_sinks || $this->new_sources; } public function addThreadData(self $taint) : void { $this->new_sinks = array_merge( $this->new_sinks, $taint->new_sinks ); $this->new_sources = array_merge( $this->new_sources, $taint->new_sources ); foreach ($taint->specializations as $id => $specializations) { if (!isset($this->specializations[$id])) { $this->specializations[$id] = $specializations; } else { $this->specializations[$id] = \array_unique( array_merge($this->specializations[$id], $specializations) ); } } } /** * @return array */ public function getFilesToAnalyze( FileReferenceProvider $reference_provider, FileStorageProvider $file_storage_provider, ClassLikeStorageProvider $classlike_storage_provider, \Psalm\Config $config ) : array { $files = []; $new_sink_file_paths = []; foreach ($this->new_sinks as $new_sink) { if ($new_sink && $new_sink->code_location) { $new_sink_file_paths[$new_sink->code_location->file_path] = $new_sink->code_location->file_path; } } foreach ($new_sink_file_paths as $file_path) { $files_referencing_file = $reference_provider->getFilesReferencingFile($file_path); $files = array_merge($files_referencing_file, $files); } $new_source_file_paths = []; foreach ($this->new_sources as $new_source) { if ($new_source && $new_source->code_location) { $new_source_file_paths[$new_source->code_location->file_path] = $new_source->code_location->file_path; } } foreach ($new_source_file_paths as $file_path) { $classlikes = $file_storage_provider->get($file_path)->classlikes_in_file; foreach ($classlikes as $classlike) { $class_storage = $classlike_storage_provider->get($classlike); if ($class_storage->location) { $files[] = $class_storage->location->file_path; } } } $files = \array_filter( $files, function ($file) use ($config) { return $config->isInProjectDirs($file); } ); $arr = \array_values($files); return \array_combine($arr, $arr); } public function clearNewSinksAndSources() : void { self::$archived_sinks = array_merge( self::$archived_sinks, $this->new_sinks ); self::$previous_sinks = $this->new_sinks; $this->new_sinks = []; self::$archived_sources = array_merge( self::$archived_sources, $this->new_sources ); self::$previous_sources = $this->new_sources; $this->new_sources = []; } }