diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index b1750133c..515402fee 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -17,6 +17,7 @@ use Psalm\Issue\TaintedInput; use Psalm\Issue\UnusedPsalmSuppress; use Psalm\Plugin\EventHandler\Event\AfterAnalysisEvent; use Psalm\Plugin\EventHandler\Event\BeforeAddIssueEvent; +use Psalm\Report\ByIssueSeverityReport; use Psalm\Report\CheckstyleReport; use Psalm\Report\CodeClimateReport; use Psalm\Report\CompactReport; @@ -854,6 +855,10 @@ final class IssueBuffer $output = new JsonReport($normalized_data, self::$fixable_issue_counts, $report_options); break; + case Report::TYPE_BY_ISSUE_SEVERITY: + $output = new ByIssueSeverityReport($normalized_data, self::$fixable_issue_counts, $report_options); + break; + case Report::TYPE_JSON_SUMMARY: $output = new JsonSummaryReport( $normalized_data, diff --git a/src/Psalm/Report.php b/src/Psalm/Report.php index a56469af0..3d2c9a463 100644 --- a/src/Psalm/Report.php +++ b/src/Psalm/Report.php @@ -29,6 +29,7 @@ abstract class Report public const TYPE_SARIF = 'sarif'; public const TYPE_CODECLIMATE = 'codeclimate'; public const TYPE_COUNT = 'count'; + const TYPE_BY_ISSUE_SEVERITY = 'by-issue-severity'; /** * @var array diff --git a/src/Psalm/Report/ByIssueSeverityReport.php b/src/Psalm/Report/ByIssueSeverityReport.php new file mode 100644 index 000000000..02116c323 --- /dev/null +++ b/src/Psalm/Report/ByIssueSeverityReport.php @@ -0,0 +1,192 @@ +sortIssuesByLevelAndType(); + + $output = ''; + + foreach ($this->issues_data as $issue_data) { + $output .= $this->format($issue_data) . "\n" . "\n"; + } + + return $output; + } + + private function format(IssueData $issue_data): string + { + $issue_string = ''; + + $is_error = $issue_data->severity === Config::REPORT_ERROR; + + if ($is_error) { + $issue_string .= ($this->use_color ? "\e[0;31mERROR\e[0m" : 'ERROR'); + } else { + $issue_string .= 'INFO'; + } + + $issue_reference = $issue_data->link ? ' (see ' . $issue_data->link . ')' : ''; + + $issue_string .= " ($issue_data->error_level): " + . $issue_data->type + . ' - ' . $this->getFileReference($issue_data) + . ' - ' . $issue_data->message . $issue_reference . "\n"; + + + if ($issue_data->taint_trace) { + $issue_string .= $this->getTaintSnippets($issue_data->taint_trace); + } elseif ($this->show_snippet) { + $snippet = $issue_data->snippet; + + if (!$this->use_color) { + $issue_string .= $snippet; + } else { + $selection_start = $issue_data->from - $issue_data->snippet_from; + $selection_length = $issue_data->to - $issue_data->from; + + $issue_string .= substr($snippet, 0, $selection_start) + . ($is_error ? "\e[97;41m" : "\e[30;47m") . substr($snippet, $selection_start, $selection_length) + . "\e[0m" . substr($snippet, $selection_length + $selection_start) . "\n"; + } + } + + if ($issue_data->other_references) { + if ($this->show_snippet) { + $issue_string .= "\n"; + } + + $issue_string .= $this->getTaintSnippets($issue_data->other_references); + } + + return $issue_string; + } + + /** + * @param non-empty-list $taint_trace + */ + private function getTaintSnippets(array $taint_trace): string + { + $snippets = ''; + + foreach ($taint_trace as $node_data) { + if ($node_data instanceof DataFlowNodeData) { + $snippets .= ' ' . $node_data->label . ' - ' . $this->getFileReference($node_data) . "\n"; + + if ($this->show_snippet) { + $snippet = $node_data->snippet; + + if (!$this->use_color) { + $snippets .= $snippet . "\n\n"; + } else { + $selection_start = $node_data->from - $node_data->snippet_from; + $selection_length = $node_data->to - $node_data->from; + + $snippets .= substr($snippet, 0, $selection_start) + . "\e[30;47m" . substr($snippet, $selection_start, $selection_length) + . "\e[0m" . substr($snippet, $selection_length + $selection_start) . "\n\n"; + } + } + } else { + $snippets .= ' ' . $node_data['label'] . "\n"; + $snippets .= ' ' . "\n\n"; + } + } + + return $snippets; + } + + /** + * @param IssueData|DataFlowNodeData $data + */ + private function getFileReference($data): string + { + $reference = $data->file_name . ':' . $data->line_from . ':' . $data->column_from; + + if (!$this->use_color) { + return $reference; + } + + $file_basename = basename($data->file_name); + $file_path = substr($data->file_name, 0, -strlen($file_basename)); + + $reference = $file_path + . "\033[1;31m" + . $file_basename . ':' . $data->line_from . ':' . $data->column_from + . "\033[0m" + ; + + if ($this->in_ci) { + return $reference; + } + + if (null === $this->link_format) { + // if xdebug is not enabled, use `get_cfg_var` to get the value directly from php.ini + $this->link_format = ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') + ?: 'file://%f#L%l'; + } + + $link = strtr($this->link_format, ['%f' => $data->file_path, '%l' => $data->line_from]); + // $reference = $data->file_name . ':' . $data->line_from . ':' . $data->column_from; + + + return "\033]8;;" . $link . "\033\\" . $reference . "\033]8;;\033\\"; + } + + /** + * @param $severity + */ + public function errorLevelMessage(int $severity): string + { + if ($severity < -1) { + return "Issues reported based on feature-specific config:"; + } + + if ($severity < 0) { + return "Issues always reported:"; + } + + return "Issues reported at error level $severity" . + ($severity === 1) ? ":" : " or less:"; + } + public function sortIssuesByLevelAndType(): void + { + usort($this->issues_data, function (IssueData $left, IssueData $right): int { + $leftLevel = $left->error_level; + $rightLevel = $right->error_level; + + if ($leftLevel != $rightLevel) { + if ($rightLevel > 0 && $leftLevel > 0) { + return $rightLevel <=> $leftLevel; + } + + if ($rightLevel > 0) { + return -1; + } + + return $leftLevel <=> $rightLevel; + } + + return $left->type <=> $right->type; + }); + } +}