Merge binding-model-{api,ri}
[yangtools.git] / parser / yang-parser-rfc7950 / src / main / java / org / opendaylight / yangtools / yang / parser / rfc7950 / antlr / IRSupport.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.parser.rfc7950.antlr;
9
10 import static com.google.common.base.Verify.verify;
11
12 import com.google.common.annotations.VisibleForTesting;
13 import com.google.common.base.CharMatcher;
14 import com.google.common.base.VerifyException;
15 import com.google.common.collect.ImmutableList;
16 import java.util.ArrayList;
17 import java.util.HashMap;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Map.Entry;
21 import java.util.function.Function;
22 import org.antlr.v4.runtime.Token;
23 import org.antlr.v4.runtime.tree.ParseTree;
24 import org.antlr.v4.runtime.tree.TerminalNode;
25 import org.eclipse.jdt.annotation.NonNull;
26 import org.opendaylight.yangtools.yang.ir.IRArgument;
27 import org.opendaylight.yangtools.yang.ir.IRArgument.Single;
28 import org.opendaylight.yangtools.yang.ir.IRKeyword;
29 import org.opendaylight.yangtools.yang.ir.IRKeyword.Qualified;
30 import org.opendaylight.yangtools.yang.ir.IRKeyword.Unqualified;
31 import org.opendaylight.yangtools.yang.ir.IRStatement;
32 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser;
33 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.ArgumentContext;
34 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.FileContext;
35 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.KeywordContext;
36 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.StatementContext;
37 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.UnquotedStringContext;
38
39 public final class IRSupport {
40     private static final CharMatcher WHITESPACE_MATCHER = CharMatcher.whitespace();
41
42     private final Map<String, Single> dquotArguments = new HashMap<>();
43     private final Map<String, Single> squotArguments = new HashMap<>();
44     private final Map<String, Single> uquotArguments = new HashMap<>();
45     private final Map<String, Single> idenArguments = new HashMap<>();
46     private final Map<String, Unqualified> uqualKeywords = new HashMap<>();
47     private final Map<Entry<String, String>, Qualified> qualKeywords = new HashMap<>();
48     private final Map<String, String> strings = new HashMap<>();
49
50     private IRSupport() {
51         // Hidden on purpose
52     }
53
54     /**
55      * Create an {@link IRStatement} from a parsed {@link FileContext}.
56      *
57      * @param file ANTLR file context
58      * @return A new IRStatement
59      * @throws NullPointerException if {@code file} is null or it does not contain a root statement
60      */
61     public static @NonNull IRStatement createStatement(@SuppressWarnings("exports") final FileContext file) {
62         return createStatement(file.statement());
63     }
64
65     /**
66      * Create an {@link IRStatement} from a parsed {@link StatementContext}.
67      *
68      * @param stmt ANTLR statement context
69      * @return A new IRStatement
70      * @throws NullPointerException if {@code stmt} is null
71      */
72     public static @NonNull IRStatement createStatement(@SuppressWarnings("exports") final StatementContext stmt) {
73         return new IRSupport().statementOf(stmt);
74     }
75
76     private @NonNull IRStatement statementOf(final StatementContext stmt) {
77         final ParseTree firstChild = stmt.getChild(0);
78         verify(firstChild instanceof KeywordContext, "Unexpected shape of %s", stmt);
79
80         final ParseTree keywordStart = firstChild.getChild(0);
81         verify(keywordStart instanceof TerminalNode, "Unexpected keyword start %s", keywordStart);
82         final Token keywordToken = ((TerminalNode) keywordStart).getSymbol();
83
84         final IRKeyword keyword = switch (firstChild.getChildCount()) {
85             case 1 -> uqualKeywords.computeIfAbsent(strOf(keywordToken), Unqualified::of);
86             case 3 -> qualKeywords.computeIfAbsent(Map.entry(strOf(keywordToken), strOf(firstChild.getChild(2))),
87                 entry -> Qualified.of(entry.getKey(), entry.getValue()));
88             default -> throw new VerifyException("Unexpected keyword " + firstChild);
89         };
90         final IRArgument argument = createArgument(stmt);
91         final ImmutableList<IRStatement> statements = createStatements(stmt);
92         final int line = keywordToken.getLine();
93         final int column = keywordToken.getCharPositionInLine();
94
95         return IRStatement.of(keyword, argument, line, column, statements);
96     }
97
98     private IRArgument createArgument(final StatementContext stmt) {
99         final ArgumentContext argument = stmt.argument();
100         if (argument == null) {
101             return null;
102         }
103         return switch (argument.getChildCount()) {
104             case 0 -> throw new VerifyException("Unexpected shape of " + argument);
105             case 1 -> createSimple(argument);
106             case 2 -> createQuoted(argument);
107             default -> createConcatenation(argument);
108         };
109     }
110
111     private IRArgument createConcatenation(final ArgumentContext argument) {
112         final var parts = new ArrayList<Single>();
113
114         for (var child : argument.children) {
115             verify(child instanceof TerminalNode, "Unexpected argument component %s", child);
116             final var token = ((TerminalNode) child).getSymbol();
117             switch (token.getType()) {
118                 case YangStatementParser.SEP:
119                     // Separator, just skip it over
120                 case YangStatementParser.PLUS:
121                     // Operator, which we are handling by concat, skip it over
122                 case YangStatementParser.DQUOT_END:
123                 case YangStatementParser.SQUOT_END:
124                     // Quote stops, skip them over because we either already added the content, or would be appending
125                     // an empty string
126                     break;
127                 case YangStatementParser.SQUOT_STRING:
128                     parts.add(createSingleQuoted(token));
129                     break;
130                 case YangStatementParser.DQUOT_STRING:
131                     parts.add(createDoubleQuoted(token));
132                     break;
133                 default:
134                     throw unexpectedToken(token);
135             }
136         }
137
138         return IRArgument.of(parts);
139     }
140
141     private Single createQuoted(final ArgumentContext argument) {
142         final var child = argument.getChild(0);
143         verify(child instanceof TerminalNode, "Unexpected literal %s", child);
144         final var token = ((TerminalNode) child).getSymbol();
145         return switch (token.getType()) {
146             case YangStatementParser.DQUOT_STRING -> createDoubleQuoted(token);
147             case YangStatementParser.SQUOT_STRING -> createSingleQuoted(token);
148             default -> throw unexpectedToken(token);
149         };
150     }
151
152     private Single createDoubleQuoted(final Token token) {
153         // Whitespace normalization happens irrespective of further handling and has no effect on the result
154         final String str = intern(trimWhitespace(token.getText(), token.getCharPositionInLine() - 1));
155
156         // TODO: turn this into a single-quoted literal if a backslash is not present. Doing so allows the
157         //       argument to be treated as a literal. See IRArgument.Single#needUnescape() for more context.
158         //       This may look unimportant, but there are scenarios where we process the same AST multiple times
159         //       and remembering this detail saves a string scan.
160
161         return dquotArguments.computeIfAbsent(str, IRArgument::doubleQuoted);
162     }
163
164     private Single createSimple(final ArgumentContext argument) {
165         final ParseTree child = argument.getChild(0);
166         if (child instanceof TerminalNode terminal) {
167             final var token = terminal.getSymbol();
168             return switch (token.getType()) {
169                 // This is as simple as it gets: we are dealing with an identifier here.
170                 case YangStatementParser.IDENTIFIER -> idenArguments.computeIfAbsent(strOf(token),
171                     IRArgument::identifier);
172                 // This is an empty string, the difference between double and single quotes does not exist. Single
173                 // quotes have more stringent semantics, hence use those.
174                 case YangStatementParser.DQUOT_END, YangStatementParser.SQUOT_END -> IRArgument.empty();
175                 default -> throw unexpectedToken(token);
176             };
177         }
178
179         verify(child instanceof UnquotedStringContext, "Unexpected shape of %s", argument);
180         // TODO: check non-presence of quotes and create a different subclass, so that ends up treated as if it
181         //       was single-quoted, i.e. bypass the check implied by IRArgument.Single#needQuoteCheck().
182         return uquotArguments.computeIfAbsent(strOf(child), IRArgument::unquoted);
183     }
184
185     private Single createSingleQuoted(final Token token) {
186         return squotArguments.computeIfAbsent(strOf(token), IRArgument::singleQuoted);
187     }
188
189     private ImmutableList<IRStatement> createStatements(final StatementContext stmt) {
190         final List<StatementContext> statements = stmt.statement();
191         return statements.isEmpty() ? ImmutableList.of()
192                 : statements.stream().map(this::statementOf).collect(ImmutableList.toImmutableList());
193     }
194
195     private String strOf(final ParseTree tree) {
196         return intern(tree.getText());
197     }
198
199     private String strOf(final Token token) {
200         return intern(token.getText());
201     }
202
203     private String intern(final String str) {
204         return strings.computeIfAbsent(str, Function.identity());
205     }
206
207     @VisibleForTesting
208     static String trimWhitespace(final String str, final int dquot) {
209         final int firstBrk = str.indexOf('\n');
210         if (firstBrk == -1) {
211             return str;
212         }
213
214         // Okay, we may need to do some trimming, set up a builder and append the first segment
215         final int length = str.length();
216         final StringBuilder sb = new StringBuilder(length);
217
218         // Append first segment, which needs only tail-trimming
219         sb.append(str, 0, trimTrailing(str, 0, firstBrk)).append('\n');
220
221         // With that out of the way, setup our iteration state. The string segment we are looking at is
222         // str.substring(start, end), which is guaranteed not to include any line breaks, i.e. end <= brk unless we are
223         // at the last segment.
224         int start = firstBrk + 1;
225         int brk = str.indexOf('\n', start);
226
227         // Loop over inner strings
228         while (brk != -1) {
229             trimLeadingAndAppend(sb, dquot, str, start, trimTrailing(str, start, brk)).append('\n');
230             start = brk + 1;
231             brk = str.indexOf('\n', start);
232         }
233
234         return trimLeadingAndAppend(sb, dquot, str, start, length).toString();
235     }
236
237     private static StringBuilder trimLeadingAndAppend(final StringBuilder sb, final int dquot, final String str,
238             final int start, final int end) {
239         int offset = start;
240         int pos = 0;
241
242         while (pos <= dquot) {
243             if (offset == end) {
244                 // We ran out of data, nothing to append
245                 return sb;
246             }
247
248             final char ch = str.charAt(offset);
249             if (ch == '\t') {
250                 // tabs are to be treated as 8 spaces
251                 pos += 8;
252             } else if (WHITESPACE_MATCHER.matches(ch)) {
253                 pos++;
254             } else {
255                 break;
256             }
257
258             offset++;
259         }
260
261         // We have expanded beyond double quotes, push equivalent spaces
262         while (pos - 1 > dquot) {
263             sb.append(' ');
264             pos--;
265         }
266
267         return sb.append(str, offset, end);
268     }
269
270     private static int trimTrailing(final String str, final int start, final int end) {
271         int ret = end;
272         while (ret > start) {
273             final int prev = ret - 1;
274             if (!WHITESPACE_MATCHER.matches(str.charAt(prev))) {
275                 break;
276             }
277             ret = prev;
278         }
279         return ret;
280     }
281
282     private static VerifyException unexpectedToken(final Token token) {
283         return new VerifyException("Unexpected token " + token);
284     }
285 }