packages/sql-catalog/src/Core/Catalog/Resolution.php

1<?php
2
3declare(strict_types=1);
4
5namespace SqlCatalog\Core\Catalog;
6
7use SqlCatalog\Core\Text\Origin;
8use SqlCatalog\Core\Text\TextPattern;
9
10/**
11 * How far the analyzer got with a statement, and why it got no further.
12 *
13 * A statement that is not pinned down is not simply "unknown". Having followed
14 * a value all the way to a request parameter is a different answer from having
15 * run out of budget, both are different from not modelling the dependency at
16 * all, and all three are different from never having looked at the call.
17 * Reporting which one it is separates what the program does from what the
18 * analyzer managed to establish.
19 *
20 * @visibility root
21 */
22enum Resolution: string
23{
24    case Resolved = 'resolved';
25    case ExternalInput = 'external-input';
26    case IncompleteModel = 'incomplete-model';
27    case Incomplete = 'incomplete';
28    case NotAnalyzed = 'not-analyzed';
29
30    /**
31     * How far the analyzer got with a statement of this shape.
32     */
33    public static function of(TextPattern $pattern): self
34    {
35        $origins = [];
36        foreach ($pattern->holes() as $hole) {
37            $origins[] = $hole->origin;
38        }
39        if ($origins === []) {
40            return self::Resolved;
41        }
42        if (in_array(Origin::Unreached, $origins, true)) {
43            return self::NotAnalyzed;
44        }
45        if (in_array(Origin::Budget, $origins, true)) {
46            return self::Incomplete;
47        }
48
49        foreach ($origins as $origin) {
50            if ($origin !== Origin::External) {
51                return self::IncompleteModel;
52            }
53        }
54
55        return self::ExternalInput;
56    }
57
58    /**
59     * Whether the statement text is pinned down.
60     */
61    public function isResolved(): bool
62    {
63        return $this === self::Resolved;
64    }
65
66    /**
67     * Whether the analyzer closed every dependency it set out to follow.
68     *
69     * Reaching runtime input closes the search: the trail was followed to its
70     * end and the string simply is not fixed. Running out of budget, or not
71     * modelling a dependency, leaves the search open.
72     */
73    public function isClosed(): bool
74    {
75        return match ($this) {
76            self::Resolved, self::ExternalInput => true,
77            self::IncompleteModel, self::Incomplete, self::NotAnalyzed => false,
78        };
79    }
80
81    /**
82     * Whether a statement was read from the call at all.
83     *
84     * A budget and a call nothing was read from both leave the text open for
85     * a reason about the analyzer rather than about the program, so what is
86     * left is not a statement with values spliced into it and must not be
87     * reported as one.
88     */
89    public function wasRead(): bool
90    {
91        return match ($this) {
92            self::Resolved, self::ExternalInput, self::IncompleteModel => true,
93            self::Incomplete, self::NotAnalyzed => false,
94        };
95    }
96
97    /**
98     * What the analyzer can say about the statement, in one line.
99     */
100    public function describe(): string
101    {
102        return match ($this) {
103            self::Resolved => 'The statement text is fully determined.',
104            self::ExternalInput => 'The values were followed to runtime input, so the text cannot be fixed.',
105            self::IncompleteModel => 'A dependency the analyzer does not model was reached.',
106            self::Incomplete => 'A cycle or an analysis budget stopped the search before it closed.',
107            self::NotAnalyzed => 'The call was found but never examined, so nothing was read from it.',
108        };
109    }
110}
111