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