2 * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
10 import static com.google.common.base.Verify.verify;
12 import com.google.common.annotations.Beta;
13 import com.google.common.annotations.VisibleForTesting;
14 import com.google.common.base.CharMatcher;
15 import com.google.common.base.VerifyException;
16 import com.google.common.collect.ImmutableList;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.List;
21 import java.util.Map.Entry;
22 import java.util.function.Function;
23 import org.antlr.v4.runtime.Token;
24 import org.antlr.v4.runtime.tree.ParseTree;
25 import org.antlr.v4.runtime.tree.TerminalNode;
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser;
28 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.ArgumentContext;
29 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.FileContext;
30 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.KeywordContext;
31 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.StatementContext;
32 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.UnquotedStringContext;
33 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Concatenation;
34 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.DoubleQuoted;
35 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Identifier;
36 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Single;
37 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.SingleQuoted;
38 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Unquoted;
39 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRKeyword.Qualified;
40 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRKeyword.Unqualified;
43 public final class AntlrSupport {
44 private static final CharMatcher WHITESPACE_MATCHER = CharMatcher.whitespace();
46 private final Map<String, DoubleQuoted> dquotArguments = new HashMap<>();
47 private final Map<String, SingleQuoted> squotArguments = new HashMap<>();
48 private final Map<String, Unquoted> uquotArguments = new HashMap<>();
49 private final Map<String, Identifier> idenArguments = new HashMap<>();
50 private final Map<String, Unqualified> uqualKeywords = new HashMap<>();
51 private final Map<Entry<String, String>, Qualified> qualKeywords = new HashMap<>();
52 private final Map<String, String> strings = new HashMap<>();
54 private AntlrSupport() {
59 * Create an {@link IRStatement} from a parsed {@link StatementContext}.
61 * @param file ANTLR file context
62 * @return A new IRStatement
63 * @throws NullPointerException if {@code file} is null or it does not contain a root statement
65 public static @NonNull IRStatement createStatement(final FileContext file) {
66 return new AntlrSupport().createStatement(file.statement());
69 private @NonNull IRStatement createStatement(final StatementContext stmt) {
70 final ParseTree firstChild = stmt.getChild(0);
71 verify(firstChild instanceof KeywordContext, "Unexpected shape of %s", stmt);
73 final ParseTree keywordStart = firstChild.getChild(0);
74 verify(keywordStart instanceof TerminalNode, "Unexpected keyword start %s", keywordStart);
75 final Token keywordToken = ((TerminalNode) keywordStart).getSymbol();
77 final IRKeyword keyword;
78 switch (firstChild.getChildCount()) {
80 keyword = uqualKeywords.computeIfAbsent(strOf(keywordToken), Unqualified::new);
83 keyword = qualKeywords.computeIfAbsent(Map.entry(strOf(keywordToken), strOf(firstChild.getChild(2))),
84 entry -> new Qualified(entry.getKey(), entry.getValue()));
87 throw new VerifyException("Unexpected keyword " + firstChild);
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();
95 switch (statements.size()) {
97 return createStatement(keyword, argument, line, column);
99 return new IRStatement144(keyword, argument, statements.get(0), line, column);
101 return new IRStatementL44(keyword, argument, statements, line, column);
105 private static @NonNull IRStatement createStatement(final IRKeyword keyword, final IRArgument argument,
106 final int line, final int column) {
107 if (line >= 0 && column >= 0) {
108 if (line <= 65535 && column <= 65535) {
109 return new IRStatement022(keyword, argument, line, column);
111 if (line <= 16777215 && column <= 255) {
112 return new IRStatement031(keyword, argument, line, column);
115 return new IRStatement044(keyword, argument, line, column);
118 private IRArgument createArgument(final StatementContext stmt) {
119 final ArgumentContext argument = stmt.argument();
120 if (argument == null) {
123 switch (argument.getChildCount()) {
125 throw new VerifyException("Unexpected shape of " + argument);
127 return createSimple(argument);
129 return createQuoted(argument);
131 return createConcatenation(argument);
135 private IRArgument createConcatenation(final ArgumentContext argument) {
136 final List<Single> parts = new ArrayList<>();
138 for (ParseTree child : argument.children) {
139 verify(child instanceof TerminalNode, "Unexpected argument component %s", child);
140 final Token token = ((TerminalNode) child).getSymbol();
141 switch (token.getType()) {
142 case YangStatementParser.SEP:
143 // Separator, just skip it over
144 case YangStatementParser.PLUS:
145 // Operator, which we are handling by concat, skip it over
146 case YangStatementParser.DQUOT_END:
147 case YangStatementParser.SQUOT_END:
148 // Quote stops, skip them over because we either already added the content, or would be appending
151 case YangStatementParser.SQUOT_STRING:
152 parts.add(createSingleQuoted(token));
154 case YangStatementParser.DQUOT_STRING:
155 parts.add(createDoubleQuoted(token));
158 throw new VerifyException("Unexpected token " + token);
162 switch (parts.size()) {
164 // A concatenation of empty strings, fall back to a single unquoted string
165 return SingleQuoted.EMPTY;
167 // A single string concatenated with empty string(s), use just the significant portion
170 // TODO: perform concatenation of single-quoted strings. For double-quoted strings this may not be as
171 // nice, but for single-quoted strings we do not need further validation in in the reactor and can
172 // use them as raw literals. This saves some indirection overhead (on memory side) and can
173 // slightly improve execution speed when we process the same IR multiple times.
174 return new Concatenation(parts);
178 private Single createQuoted(final ArgumentContext argument) {
179 final ParseTree child = argument.getChild(0);
180 verify(child instanceof TerminalNode, "Unexpected literal %s", child);
181 final Token token = ((TerminalNode) child).getSymbol();
182 switch (token.getType()) {
183 case YangStatementParser.DQUOT_STRING:
184 return createDoubleQuoted(token);
185 case YangStatementParser.SQUOT_STRING:
186 return createSingleQuoted(token);
188 throw new VerifyException("Unexpected token " + token);
192 private DoubleQuoted createDoubleQuoted(final Token token) {
193 // Whitespace normalization happens irrespective of further handling and has no effect on the result
194 final String str = intern(trimWhitespace(token.getText(), token.getCharPositionInLine() - 1));
196 // TODO: turn this into a single-quoted literal if a backslash is not present. Doing so allows the
197 // argument to be treated as a literal. See IRArgument.Single#needUnescape() for more context.
198 // This may look unimportant, but there are scenarios where we process the same AST multiple times
199 // and remembering this detail saves a string scan.
201 return dquotArguments.computeIfAbsent(str, DoubleQuoted::new);
204 private IRArgument createSimple(final ArgumentContext argument) {
205 final ParseTree child = argument.getChild(0);
206 if (child instanceof TerminalNode) {
207 final Token token = ((TerminalNode) child).getSymbol();
208 switch (token.getType()) {
209 case YangStatementParser.IDENTIFIER:
210 // This is as simple as it gets: we are dealing with an identifier here.
211 return idenArguments.computeIfAbsent(strOf(token), Identifier::new);
212 case YangStatementParser.DQUOT_END:
213 case YangStatementParser.SQUOT_END:
214 // This is an empty string, the difference between double and single quotes does not exist. Single
215 // quotes have more stringent semantics, hence use those.
216 return SingleQuoted.EMPTY;
218 throw new VerifyException("Unexpected token " + token);
222 verify(child instanceof UnquotedStringContext, "Unexpected shape of %s", argument);
223 // TODO: check non-presence of quotes and create a different subclass, so that ends up treated as if it
224 // was single-quoted, i.e. bypass the check implied by IRArgument.Single#needQuoteCheck().
225 return uquotArguments.computeIfAbsent(strOf(child), Unquoted::new);
228 private SingleQuoted createSingleQuoted(final Token token) {
229 return squotArguments.computeIfAbsent(strOf(token), SingleQuoted::new);
232 private ImmutableList<IRStatement> createStatements(final StatementContext stmt) {
233 final List<StatementContext> statements = stmt.statement();
234 return statements.isEmpty() ? ImmutableList.of()
235 : statements.stream().map(this::createStatement).collect(ImmutableList.toImmutableList());
238 private String strOf(final ParseTree tree) {
239 return intern(tree.getText());
242 private String strOf(final Token token) {
243 return intern(token.getText());
246 private String intern(final String str) {
247 return strings.computeIfAbsent(str, Function.identity());
251 static String trimWhitespace(final String str, final int dquot) {
252 final int firstBrk = str.indexOf('\n');
253 if (firstBrk == -1) {
257 // Okay, we may need to do some trimming, set up a builder and append the first segment
258 final int length = str.length();
259 final StringBuilder sb = new StringBuilder(length);
261 // Append first segment, which needs only tail-trimming
262 sb.append(str, 0, trimTrailing(str, 0, firstBrk)).append('\n');
264 // With that out of the way, setup our iteration state. The string segment we are looking at is
265 // str.substring(start, end), which is guaranteed not to include any line breaks, i.e. end <= brk unless we are
266 // at the last segment.
267 int start = firstBrk + 1;
268 int brk = str.indexOf('\n', start);
270 // Loop over inner strings
272 trimLeadingAndAppend(sb, dquot, str, start, trimTrailing(str, start, brk)).append('\n');
274 brk = str.indexOf('\n', start);
277 return trimLeadingAndAppend(sb, dquot, str, start, length).toString();
280 private static StringBuilder trimLeadingAndAppend(final StringBuilder sb, final int dquot, final String str,
281 final int start, final int end) {
285 while (pos <= dquot) {
287 // We ran out of data, nothing to append
291 final char ch = str.charAt(offset);
293 // tabs are to be treated as 8 spaces
295 } else if (WHITESPACE_MATCHER.matches(ch)) {
304 // We have expanded beyond double quotes, push equivalent spaces
305 while (pos - 1 > dquot) {
310 return sb.append(str, offset, end);
313 private static int trimTrailing(final String str, final int start, final int end) {
315 while (ret > start) {
316 final int prev = ret - 1;
317 if (!WHITESPACE_MATCHER.matches(str.charAt(prev))) {