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.antlr;
10 import static com.google.common.base.Verify.verify;
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;
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;
39 public final class IRSupport {
40 private static final CharMatcher WHITESPACE_MATCHER = CharMatcher.whitespace();
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<>();
55 * Create an {@link IRStatement} from a parsed {@link FileContext}.
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
61 public static @NonNull IRStatement createStatement(@SuppressWarnings("exports") final FileContext file) {
62 return createStatement(file.statement());
66 * Create an {@link IRStatement} from a parsed {@link StatementContext}.
68 * @param stmt ANTLR statement context
69 * @return A new IRStatement
70 * @throws NullPointerException if {@code stmt} is null
72 public static @NonNull IRStatement createStatement(@SuppressWarnings("exports") final StatementContext stmt) {
73 return new IRSupport().statementOf(stmt);
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);
80 final ParseTree keywordStart = firstChild.getChild(0);
81 verify(keywordStart instanceof TerminalNode, "Unexpected keyword start %s", keywordStart);
82 final Token keywordToken = ((TerminalNode) keywordStart).getSymbol();
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);
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 return IRStatement.of(keyword, argument, line, column, statements);
98 private IRArgument createArgument(final StatementContext stmt) {
99 final ArgumentContext argument = stmt.argument();
100 if (argument == null) {
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);
111 private IRArgument createConcatenation(final ArgumentContext argument) {
112 final var parts = new ArrayList<Single>();
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
127 case YangStatementParser.SQUOT_STRING:
128 parts.add(createSingleQuoted(token));
130 case YangStatementParser.DQUOT_STRING:
131 parts.add(createDoubleQuoted(token));
134 throw unexpectedToken(token);
138 return IRArgument.of(parts);
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);
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));
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.
161 return dquotArguments.computeIfAbsent(str, IRArgument::doubleQuoted);
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);
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);
185 private Single createSingleQuoted(final Token token) {
186 return squotArguments.computeIfAbsent(strOf(token), IRArgument::singleQuoted);
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());
195 private String strOf(final ParseTree tree) {
196 return intern(tree.getText());
199 private String strOf(final Token token) {
200 return intern(token.getText());
203 private String intern(final String str) {
204 return strings.computeIfAbsent(str, Function.identity());
208 static String trimWhitespace(final String str, final int dquot) {
209 final int firstBrk = str.indexOf('\n');
210 if (firstBrk == -1) {
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);
218 // Append first segment, which needs only tail-trimming
219 sb.append(str, 0, trimTrailing(str, 0, firstBrk)).append('\n');
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);
227 // Loop over inner strings
229 trimLeadingAndAppend(sb, dquot, str, start, trimTrailing(str, start, brk)).append('\n');
231 brk = str.indexOf('\n', start);
234 return trimLeadingAndAppend(sb, dquot, str, start, length).toString();
237 private static StringBuilder trimLeadingAndAppend(final StringBuilder sb, final int dquot, final String str,
238 final int start, final int end) {
242 while (pos <= dquot) {
244 // We ran out of data, nothing to append
248 final char ch = str.charAt(offset);
250 // tabs are to be treated as 8 spaces
252 } else if (WHITESPACE_MATCHER.matches(ch)) {
261 // We have expanded beyond double quotes, push equivalent spaces
262 while (pos - 1 > dquot) {
267 return sb.append(str, offset, end);
270 private static int trimTrailing(final String str, final int start, final int end) {
272 while (ret > start) {
273 final int prev = ret - 1;
274 if (!WHITESPACE_MATCHER.matches(str.charAt(prev))) {
282 private static VerifyException unexpectedToken(final Token token) {
283 return new VerifyException("Unexpected token " + token);