1<?php
2
3declare(strict_types=1);
4
5namespace Antlr\Antlr4\Runtime\Error\Listeners;
6
7use Antlr\Antlr4\Runtime\Atn\ATNConfigSet;
8use Antlr\Antlr4\Runtime\Dfa\DFA;
9use Antlr\Antlr4\Runtime\Interval;
10use Antlr\Antlr4\Runtime\Parser;
11use Antlr\Antlr4\Runtime\Utils\BitSet;
12
13/**
14 * This implementation of {@see ANTLRErrorListener} can be used to identify
15 * certain potential correctness and performance problems in grammars. "Reports"
16 * are made by calling {@see Parser::notifyErrorListeners()} with the appropriate
17 * message.
18 *
19 * - Ambiguities: These are cases where more than one path through the
20 * grammar can match the input.
21 *
22 * - Weak context sensitivity: These are cases where full-context
23 * prediction resolved an SLL conflict to a unique alternative which equaled the
24 * minimum alternative of the SLL conflict.
25 *
26 * - Strong (forced) context sensitivity: These are cases where the
27 * full-context prediction resolved an SLL conflict to a unique alternative,
28 * and the minimum alternative of the SLL conflict was found to not be
29 * a truly viable alternative. Two-stage parsing cannot be used for inputs where
30 * this situation occurs.
31 *
32 * @author Sam Harwell
33 */
34class DiagnosticErrorListener extends BaseErrorListener
35{
36    /**
37     * When `true`, only exactly known ambiguities are reported.
38     *
39     * @var bool
40     */
41    protected $exactOnly;
42
43    public function __construct(bool $exactOnly = true)
44    {
45        $this->exactOnly = $exactOnly;
46    }
47
48    public function reportAmbiguity(
49        Parser $recognizer,
50        DFA $dfa,
51        int $startIndex,
52        int $stopIndex,
53        bool $exact,
54        ?BitSet $ambigAlts,
55        ATNConfigSet $configs
56    ) : void {
57        if ($this->exactOnly && !$exact) {
58            return;
59        }
60
61        $tokenStream = $recognizer->getTokenStream();
62
63        $msg = \sprintf(
64            'reportAmbiguity d=%s: ambigAlts=%s, input=\'%s\'',
65            $this->getDecisionDescription($recognizer, $dfa),
66            $this->getConflictingAlts($ambigAlts, $configs),
67            $tokenStream === null ? '' : $tokenStream->getTextByInterval(new Interval($startIndex, $stopIndex))
68        );
69
70        $recognizer->notifyErrorListeners($msg);
71    }
72
73    public function reportAttemptingFullContext(
74        Parser $recognizer,
75        DFA $dfa,
76        int $startIndex,
77        int $stopIndex,
78        ?BitSet $conflictingAlts,
79        ATNConfigSet $configs
80    ) : void {
81        $tokenStream = $recognizer->getTokenStream();
82
83        $msg = \sprintf(
84            'reportAttemptingFullContext d=%s, input=\'%s\'',
85            $this->getDecisionDescription($recognizer, $dfa),
86            $tokenStream === null ? '' : $tokenStream->getTextByInterval(new Interval($startIndex, $stopIndex))
87        );
88
89        $recognizer->notifyErrorListeners($msg);
90    }
91
92    public function reportContextSensitivity(
93        Parser $recognizer,
94        DFA $dfa,
95        int $startIndex,
96        int $stopIndex,
97        int $prediction,
98        ATNConfigSet $configs
99    ) : void {
100        $tokenStream = $recognizer->getTokenStream();
101
102        $msg = \sprintf(
103            'reportContextSensitivity d=%s, input=\'%s\'',
104            $this->getDecisionDescription($recognizer, $dfa),
105            $tokenStream === null ? '' : $tokenStream->getTextByInterval(new Interval($startIndex, $stopIndex))
106        );
107
108        $recognizer->notifyErrorListeners($msg);
109    }
110
111    protected function getDecisionDescription(Parser $recognizer, DFA $dfa) : string
112    {
113        $decision = $dfa->decision;
114
115        if ($dfa->atnStartState === null) {
116            throw new \RuntimeException('Unexpected null ATN Start State.');
117        }
118
119        $ruleIndex = $dfa->atnStartState->ruleIndex;
120
121        $ruleNames = $recognizer->getRuleNames();
122
123        if ($ruleIndex < 0 || $ruleIndex >= \count($ruleNames)) {
124            return (string) $decision;
125        }
126
127        $ruleName = $ruleNames[$ruleIndex];
128
129        if (\strlen($ruleName) === 0) {
130            return (string) $decision;
131        }
132
133        return \sprintf('%d (%s)', $decision, $ruleName);
134    }
135
136    /**
137     * Computes the set of conflicting or ambiguous alternatives from a
138     * configuration set, if that information was not already provided by the
139     * parser.
140     *
141     * @param BitSet|null  $reportedAlts The set of conflicting or ambiguous
142     *                                   alternatives, as reported by the parser.
143     * @param ATNConfigSet $configs      The conflicting or ambiguous
144     *                                   configuration set.
145     *
146     * @return BitSet `reportedAlts` if it is not `null`, otherwise returns
147     *                the set of alternatives represented in `configs`.
148     */
149    protected function getConflictingAlts(?BitSet $reportedAlts, ATNConfigSet $configs) : BitSet
150    {
151        if ($reportedAlts !== null) {
152            return $reportedAlts;
153        }
154
155        $result = new BitSet();
156        foreach ($configs->configs as $config) {
157            $result->add($config->alt);
158        }
159
160        return $result;
161    }
162}
163