Add an explicit intermediate YANG representation
[yangtools.git] / yang / yang-parser-rfc7950 / src / main / java / org / opendaylight / yangtools / yang / parser / rfc7950 / ir / StatementFactory.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.ir;
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.HashMap;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.Map.Entry;
20 import java.util.function.Function;
21 import org.antlr.v4.runtime.Token;
22 import org.antlr.v4.runtime.tree.ParseTree;
23 import org.antlr.v4.runtime.tree.TerminalNode;
24 import org.eclipse.jdt.annotation.NonNull;
25 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser;
26 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.ArgumentContext;
27 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.KeywordContext;
28 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.QuotedStringContext;
29 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.StatementContext;
30 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.UnquotedStringContext;
31 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Concatenation;
32 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.DoubleQuoted;
33 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Identifier;
34 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Single;
35 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.SingleQuoted;
36 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Unquoted;
37 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRKeyword.Qualified;
38 import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRKeyword.Unqualified;
39
40 final class StatementFactory {
41     private static final CharMatcher WHITESPACE_MATCHER = CharMatcher.whitespace();
42
43     private final Map<String, DoubleQuoted> dquotArguments = new HashMap<>();
44     private final Map<String, SingleQuoted> squotArguments = new HashMap<>();
45     private final Map<String, Unquoted> uquotArguments = new HashMap<>();
46     private final Map<String, Identifier> idenArguments = new HashMap<>();
47     private final Map<String, Unqualified> uqualKeywords = new HashMap<>();
48     private final Map<Entry<String, String>, Qualified> qualKeywords = new HashMap<>();
49     private final Map<String, String> strings = new HashMap<>();
50
51     @NonNull IRStatement createStatement(final StatementContext stmt) {
52         final ParseTree firstChild = stmt.getChild(0);
53         verify(firstChild instanceof KeywordContext, "Unexpected shape of %s", stmt);
54
55         final ParseTree keywordStart = firstChild.getChild(0);
56         verify(keywordStart instanceof TerminalNode, "Unexpected keyword start %s", keywordStart);
57         final Token keywordToken = ((TerminalNode) keywordStart).getSymbol();
58
59         final IRKeyword keyword;
60         switch (firstChild.getChildCount()) {
61             case 1:
62                 keyword = uqualKeywords.computeIfAbsent(strOf(keywordToken), Unqualified::new);
63                 break;
64             case 3:
65                 keyword = qualKeywords.computeIfAbsent(Map.entry(strOf(keywordToken), strOf(firstChild.getChild(2))),
66                     entry -> new Qualified(entry.getKey(), entry.getValue()));
67                 break;
68             default:
69                 throw new VerifyException("Unexpected keyword " + firstChild);
70         }
71
72         final IRArgument argument = createArgument(stmt);
73         final ImmutableList<IRStatement> statements = createStatements(stmt);
74         final int line = keywordToken.getLine();
75         final int column = keywordToken.getCharPositionInLine();
76
77         switch (statements.size()) {
78             case 0:
79                 return createStatement(keyword, argument, line, column);
80             case 1:
81                 return new IRStatement144(keyword, argument, statements.get(0), line, column);
82             default:
83                 return new IRStatementL44(keyword, argument, statements, line, column);
84         }
85     }
86
87     private static @NonNull IRStatement createStatement(final IRKeyword keyword, final IRArgument argument,
88             final int line, final int column) {
89         if (line >= 0 && column >= 0) {
90             if (line <= 65535 && column <= 65535) {
91                 return new IRStatement022(keyword, argument, line, column);
92             }
93             if (line <= 16777215 && column <= 255) {
94                 return new IRStatement031(keyword, argument, line, column);
95             }
96         }
97         return new IRStatement044(keyword, argument, line, column);
98     }
99
100     private IRArgument createArgument(final StatementContext stmt) {
101         final ArgumentContext argCtx = stmt.argument();
102         if (argCtx == null) {
103             return null;
104         }
105         if (argCtx.getChildCount() == 1) {
106             final ParseTree child = argCtx.getChild(0);
107             if (child instanceof TerminalNode) {
108                 // This is as simple as it gets: we are dealing with an identifier here.
109                 return idenArguments.computeIfAbsent(strOf(((TerminalNode) child).getSymbol()), Identifier::new);
110             }
111             if (child instanceof UnquotedStringContext) {
112                 // TODO: check non-presence of quotes and create a different subclass, so that ends up treated as if it
113                 //       was single-quoted, i.e. bypass the check implied by IRArgument.Single#needQuoteCheck().
114                 return uquotArguments.computeIfAbsent(strOf(child), Unquoted::new);
115             }
116
117             verify(child instanceof QuotedStringContext, "Unexpected child %s", child);
118             return createArgument((QuotedStringContext) child);
119         }
120
121         // TODO: perform concatenation of single-quoted strings. For double-quoted strings this may not be as nice, but
122         //       for single-quoted strings we do not need further validation in in the reactor and can use them as raw
123         //       literals. This saves some indirection overhead (on memory side) and can slightly improve execution
124         //       speed when we process the same IR multiple times.
125
126         return new Concatenation(argCtx.quotedString().stream().map(this::createArgument)
127             .collect(ImmutableList.toImmutableList()));
128     }
129
130     private Single createArgument(final QuotedStringContext argument) {
131         final ParseTree literal = argument.getChild(1);
132         verify(literal instanceof TerminalNode, "Unexpected literal %s", literal);
133         final Token token = ((TerminalNode) literal).getSymbol();
134         switch (token.getType()) {
135             case YangStatementParser.DQUOT_END:
136                 return dquotArguments.computeIfAbsent("", DoubleQuoted::new);
137             case YangStatementParser.DQUOT_STRING:
138                 // Whitespace normalization happens irrespective of further handling and has no effect on the result
139                 final String str = intern(trimWhitespace(token.getText(), token.getCharPositionInLine() - 1));
140
141                 // TODO: turn this into a single-quoted literal if a backslash is not present. Doing so allows the
142                 //       argument to be treated as a literal. See IRArgument.Single#needUnescape() for more context.
143                 //       This may look unimportant, but there are scenarios where we process the same AST multiple times
144                 //       and remembering this detail saves a string scan.
145
146                 return dquotArguments.computeIfAbsent(str, DoubleQuoted::new);
147             case YangStatementParser.SQUOT_END:
148                 return squotArguments.computeIfAbsent("", SingleQuoted::new);
149             case YangStatementParser.SQUOT_STRING:
150                 return squotArguments.computeIfAbsent(strOf(token), SingleQuoted::new);
151             default:
152                 throw new VerifyException("Unexpected token " + token);
153         }
154     }
155
156     private ImmutableList<IRStatement> createStatements(final StatementContext stmt) {
157         final List<StatementContext> statements = stmt.statement();
158         return statements.isEmpty() ? ImmutableList.of()
159                 : statements.stream().map(this::createStatement).collect(ImmutableList.toImmutableList());
160     }
161
162     private String strOf(final ParseTree tree) {
163         return intern(tree.getText());
164     }
165
166     private String strOf(final Token token) {
167         return intern(token.getText());
168     }
169
170     private String intern(final String str) {
171         return strings.computeIfAbsent(str, Function.identity());
172     }
173
174     @VisibleForTesting
175     static String trimWhitespace(final String str, final int dquot) {
176         final int firstBrk = str.indexOf('\n');
177         if (firstBrk == -1) {
178             return str;
179         }
180
181         // Okay, we may need to do some trimming, set up a builder and append the first segment
182         final int length = str.length();
183         final StringBuilder sb = new StringBuilder(length);
184
185         // Append first segment, which needs only tail-trimming
186         sb.append(str, 0, trimTrailing(str, 0, firstBrk)).append('\n');
187
188         // With that out of the way, setup our iteration state. The string segment we are looking at is
189         // str.substring(start, end), which is guaranteed not to include any line breaks, i.e. end <= brk unless we are
190         // at the last segment.
191         int start = firstBrk + 1;
192         int brk = str.indexOf('\n', start);
193
194         // Loop over inner strings
195         while (brk != -1) {
196             trimLeadingAndAppend(sb, dquot, str, start, trimTrailing(str, start, brk)).append('\n');
197             start = brk + 1;
198             brk = str.indexOf('\n', start);
199         }
200
201         return trimLeadingAndAppend(sb, dquot, str, start, length).toString();
202     }
203
204     private static StringBuilder trimLeadingAndAppend(final StringBuilder sb, final int dquot, final String str,
205             final int start, final int end) {
206         int offset = start;
207         int pos = 0;
208
209         while (pos <= dquot) {
210             if (offset == end) {
211                 // We ran out of data, nothing to append
212                 return sb;
213             }
214
215             final char ch = str.charAt(offset);
216             if (ch == '\t') {
217                 // tabs are to be treated as 8 spaces
218                 pos += 8;
219             } else if (WHITESPACE_MATCHER.matches(ch)) {
220                 pos++;
221             } else {
222                 break;
223             }
224
225             offset++;
226         }
227
228         // We have expanded beyond double quotes, push equivalent spaces
229         while (pos - 1 > dquot) {
230             sb.append(' ');
231             pos--;
232         }
233
234         return sb.append(str, offset, end);
235     }
236
237     private static int trimTrailing(final String str, final int start, final int end) {
238         int ret = end;
239         while (ret > start) {
240             final int prev = ret - 1;
241             if (!WHITESPACE_MATCHER.matches(str.charAt(prev))) {
242                 break;
243             }
244             ret = prev;
245         }
246         return ret;
247     }
248 }