packages/sql-catalog/src/Reporter/Text/TextReporter.php

1<?php
2
3declare(strict_types=1);
4
5namespace SqlCatalog\Reporter\Text;
6
7use Override;
8use SqlCatalog\Core\Catalog\Catalog;
9use SqlCatalog\Core\Catalog\CatalogEntry;
10use SqlCatalog\Core\Reporter\CatalogArtifacts;
11use SqlCatalog\Core\Reporter\ReporterInterface;
12
13/**
14 * Writes the catalog as plain lines, for reading in a terminal.
15 *
16 * @visibility root
17 */
18final class TextReporter implements ReporterInterface
19{
20    /**
21     * The name the artifact is written under.
22     */
23    public const FILE = 'catalog.txt';
24
25    /**
26     * The name the command line selects this reporter by.
27     */
28    #[Override]
29    public function name(): string
30    {
31        return 'text';
32    }
33
34    /**
35     * What the reporter produces.
36     */
37    #[Override]
38    public function description(): string
39    {
40        return 'one block per statement, for reading in a terminal';
41    }
42
43    /**
44     * The catalog rendered as plain text.
45     */
46    #[Override]
47    public function render(Catalog $catalog): CatalogArtifacts
48    {
49        $lines = [];
50        foreach ($catalog->sorted() as $entry) {
51            foreach ($this->entryLines($entry) as $line) {
52                $lines[] = $line;
53            }
54            $lines[] = '';
55        }
56        foreach ($catalog->sorted()->problems() as $problem) {
57            $lines[] = sprintf('! %s: %s', $problem->file, $problem->message);
58        }
59        $lines[] = 'Conditions are not evaluated; runtime reachability is not assessed.';
60        $lines[] = $this->summaryLine($catalog);
61
62        return CatalogArtifacts::one(self::FILE, implode("\n", $lines) . "\n");
63    }
64
65    /**
66     * The lines one statement is written as.
67     *
68     * @return list<string>
69     */
70    public function entryLines(CatalogEntry $entry): array
71    {
72        $lines = [
73            sprintf('%s  %s  %s', $entry->site->display(), strtoupper($entry->kind->value), $entry->id),
74            sprintf('  in %s via %s', $entry->site->function, $entry->site->sink),
75            '  ' . $entry->sql(),
76            '  ' . $this->statusLine($entry),
77        ];
78        foreach ($entry->placeholders as $placeholder) {
79            $lines[] = sprintf('  %s = %s', $placeholder->token, $placeholder->value?->display() ?? '(unbound)');
80        }
81        foreach ($entry->findings as $finding) {
82            $lines[] = sprintf('  [%s] %s %s', strtoupper($finding->severity->value), $finding->rule->value, $finding->message);
83        }
84
85        return $lines;
86    }
87
88    /**
89     * The line saying how far the analyzer got with a statement.
90     */
91    public function statusLine(CatalogEntry $entry): string
92    {
93        $status = [$entry->resolution()->value];
94        if (!$entry->searchClosed()) {
95            $status[] = 'search did not close';
96        }
97        if (!$entry->correlated) {
98            $status[] = 'alternatives may be unreachable';
99        }
100        if (count($entry->through) > 1) {
101            $status[] = 'via ' . implode(' -> ', $entry->through);
102        }
103
104        return implode('; ', $status);
105    }
106
107    /**
108     * The closing line naming what the run found.
109     */
110    public function summaryLine(Catalog $catalog): string
111    {
112        $exact = 0;
113        $findings = 0;
114        foreach ($catalog as $entry) {
115            $exact += $entry->isExact() ? 1 : 0;
116            $findings += count($entry->findings);
117        }
118
119        return sprintf(
120            '%d statement(s), %d fully resolved, %d finding(s), %d unreadable file(s).',
121            $catalog->count(),
122            $exact,
123            $findings,
124            count($catalog->problems()),
125        );
126    }
127}
128