*/
package org.opendaylight.yangtools.yang.parser.repo;
-import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFluentFuture;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;
-import org.antlr.v4.runtime.ParserRuleContext;
import org.eclipse.jdt.annotation.NonNull;
import org.gaul.modernizer_maven_annotations.SuppressModernizer;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
import org.opendaylight.yangtools.yang.model.repo.api.SchemaResolutionException;
import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
import org.opendaylight.yangtools.yang.model.repo.api.StatementParserMode;
-import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.StatementContext;
import org.opendaylight.yangtools.yang.parser.rfc7950.repo.ASTSchemaSource;
import org.opendaylight.yangtools.yang.parser.rfc7950.repo.YangModelDependencyInfo;
import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException;
for (final Entry<SourceIdentifier, ASTSchemaSource> entry : srcs.entrySet()) {
final ASTSchemaSource ast = entry.getValue();
- final ParserRuleContext parserRuleCtx = ast.getAST();
- checkArgument(parserRuleCtx instanceof StatementContext, "Unsupported context class %s for source %s",
- parserRuleCtx.getClass(), entry.getKey());
-
try {
- parser.addSource(entry.getValue());
+ parser.addSource(ast);
} catch (YangSyntaxErrorException | IOException e) {
throw new SchemaResolutionException("Failed to add source " + entry.getKey(), e);
}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
+
+import com.google.common.base.MoreObjects;
+import org.opendaylight.yangtools.concepts.Immutable;
+
+abstract class AbstractIRObject implements Immutable {
+ @Override
+ public final String toString() {
+ return MoreObjects.toStringHelper(this).add("fragment", toYangFragment(new StringBuilder())).toString();
+ }
+
+ abstract StringBuilder toYangFragment(StringBuilder sb);
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableList;
+import java.util.Iterator;
+import java.util.List;
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * An argument to a YANG statement, as defined by section 6.1.3 of both
+ * <a href="https://tools.ietf.org/html/rfc6020#section-6.1.3">RFC6020</a> and
+ * <a href="https://tools.ietf.org/html/rfc7950#section-6.1.3">RFC7950</a>. An argument is effectively any old string,
+ * except it can be defined in a number of ways:
+ * <ul>
+ * <li>it can be a simple unquoted string, or</li>
+ * <li>it can be a single-quoted string, with its contents being completely preserved, or</li>
+ * <li>it can be a double-quoted string, which defines some escaping and whitespace-stripping rules, or</li>
+ * <li>it can be a concatenation of any number of single- or double-quoted strings</li>
+ * </ul>
+ *
+ * <p>
+ * The first three cases as covered by {@link Single} subclass, which exposes appropriate methods to infer how its
+ * string literal is to be interpreted. The last case is handled by {@link Concatenation} subclass, which exposes
+ * the constituent parts as {@link Single} items.
+ *
+ * <p>
+ * Please note that parser implementations producing these argument representations are <b>NOT</b> required to retain
+ * the format of the original definition. They are free to perform quoting and concatenation transformations as long as
+ * they maintain semantic equivalence. As a matter of example, these transformations are explicitly allowed:
+ * <ul>
+ * <li>elimination of unneeded quotes, for example turning {@code "foo"} into {@code foo}</li>
+ * <li>transformation of quotes, for example turning {@code "foo\nbar"} into {@code 'foo
bar'}</li>
+ * <li>concatenation processing, for example turning {@code 'foo' + 'bar'} into {@code foobar}</li>
+ * </ul>
+ */
+@Beta
+public abstract class IRArgument extends AbstractIRObject {
+ /**
+ * An argument composed of multiple concatenated parts.
+ */
+ public static final class Concatenation extends IRArgument {
+ private final @NonNull ImmutableList<Single> parts;
+
+ Concatenation(final ImmutableList<Single> parts) {
+ this.parts = requireNonNull(parts);
+ }
+
+ /**
+ * Return the argument parts that need to be concatenated.
+ *
+ * @return Argument parts.
+ */
+ public @NonNull List<? extends Single> parts() {
+ return parts;
+ }
+
+ @Override
+ StringBuilder toYangFragment(final StringBuilder sb) {
+ final Iterator<Single> it = parts.iterator();
+ it.next().toYangFragment(sb);
+ while (it.hasNext()) {
+ it.next().toYangFragment(sb.append(" + "));
+ }
+ return sb;
+ }
+ }
+
+ /**
+ * An argument composed of a single string. This string may need further validation and processing, as it may not
+ * actually conform to the specification as requested by {@code yang-version}.
+ */
+ /*
+ * This is the public footprint which is served by three final subclasses: DoublyQuoted, SingleQuoted, Unquoted.
+ * Those classes must never be exposed, as they are a manifestation of current implementation in StatementFactory.
+ * As noted in the interface contract of IRArgument, we have very much free reign on syntactic transformations,
+ * StatementFactory is just not taking advantage of those at this point.
+ *
+ * The subclasses may very much change, in terms of both naming and function, to support whatever StatementFactory
+ * ends up doing.
+ */
+ public abstract static class Single extends IRArgument {
+ private final @NonNull String string;
+
+ Single(final String string) {
+ this.string = requireNonNull(string);
+ }
+
+ /**
+ * Significant portion of this argument. For unquoted and single-quoted strings this is the unquoted string
+ * literal. For double-quoted strings this is the unquoted string, after whitespace trimming as defined by
+ * RFC6020/RFC7950 section 6.1.3, but before escape substitution.
+ *
+ * @return Significant portion of this argument.
+ */
+ public final @NonNull String string() {
+ return string;
+ }
+
+ /**
+ * Imprecise check if this argument needs further unescape operation (which is version-specific) to arrive at
+ * the literal string value. This is false for unquoted and single-quoted strings, which do not support any sort
+ * of escaping. This may be true for double-quoted strings, as they <b>may</b> need to be further processed in
+ * version-dependent ways to arrive at the correct literal value.
+ *
+ * <p>
+ * This method is allowed to err on the false-positive side -- i.e. it may report any double-quoted string as
+ * needing further processing, even when the actual content could be determined to not need further processing.
+ *
+ * @return False if the value of {@link #string} can be used as-is.
+ */
+ public final boolean needUnescape() {
+ return this instanceof DoubleQuoted;
+ }
+
+ /**
+ * Imprecise check if this argument needs an additional content check for compliance. This is false if the
+ * string was explicitly quoted and therefore cannot contain stray single- or double-quotes, or if the content
+ * has already been checked to not contain them.
+ *
+ * <p>
+ * The content check is needed to ascertain RFC7950 compliance, because RFC6020 allows constructs like
+ * <pre>abc"def</pre> in unquoted strings, while RFC7950 explicitly forbids them.
+ *
+ * <p>
+ * This method is allowed to err on the false-positive side -- i.e. it may report any unquoted string as
+ * needing this check, even when the actual content could be determined to not contain quotes.
+ *
+ * @return True if this argument requires a version-specific check for quote content.
+ */
+ public final boolean needQuoteCheck() {
+ return this instanceof Unquoted;
+ }
+
+ /**
+ * Imprecise check if this argument complies with the {@code identifier} YANG specification.
+ *
+ * <p>
+ * This method is allowed to err on the false-negative side -- i.e. it may report any string as not being
+ * compliant with {@code identifier}, even when the actual content could be determined to be compliant.
+ *
+ * @return True if this argument is known to be directly usable in contexts where YANG requires the use of
+ */
+ public final boolean isValidIdentifier() {
+ return this instanceof Identifier;
+ }
+
+ @Override
+ StringBuilder toYangFragment(final StringBuilder sb) {
+ return sb.append(string);
+ }
+ }
+
+ static final class DoubleQuoted extends Single {
+ DoubleQuoted(final String string) {
+ super(string);
+ }
+
+ @Override
+ StringBuilder toYangFragment(final StringBuilder sb) {
+ // Note this is just an approximation. We do not have enough state knowledge to restore any whitespace we
+ // may have trimmed.
+ return super.toYangFragment(sb.append('"')).append('"');
+ }
+ }
+
+ static final class SingleQuoted extends Single {
+ SingleQuoted(final String string) {
+ super(string);
+ }
+
+ @Override
+ StringBuilder toYangFragment(final StringBuilder sb) {
+ return super.toYangFragment(sb.append('\'')).append('\'');
+ }
+ }
+
+ static final class Identifier extends Single {
+ Identifier(final String string) {
+ super(string);
+ }
+ }
+
+ static final class Unquoted extends Single {
+ Unquoted(final String string) {
+ super(string);
+ }
+ }
+
+ IRArgument() {
+ // Hidden on purpose
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.common.AbstractQName;
+
+/**
+ * A YANG keyword, as defined, as defined by section 6.1.2 of both
+ * <a href="https://tools.ietf.org/html/rfc6020#section-6.1.2">RFC6020</a> and
+ * <a href="https://tools.ietf.org/html/rfc7950#section-6.1.2">RFC7950</a>. The two options are discerned by nullability
+ * of {@link #prefix()} method's return, as hinted by the ABNF for {@code node-identifier} -- and while a keyword is a
+ * semantically different construct, it shares the same value space.
+ *
+ * <p>
+ * Naming in this class prefers the formal ABNF specification and draws value-space and type-safety implications from
+ * that connection, rather than following the RFC-assigned names.
+ */
+@Beta
+public abstract class IRKeyword extends AbstractIRObject {
+ @Beta
+ public static final class Qualified extends IRKeyword {
+ private final @NonNull String prefix;
+
+ Qualified(final String prefix, final String localName) {
+ super(localName);
+ this.prefix = requireNonNull(prefix);
+ }
+
+ @Override
+ public @NonNull String prefix() {
+ return prefix;
+ }
+
+ @Override
+ public String asStringDeclaration() {
+ return prefix + ':' + identifier();
+ }
+
+ @Override
+ StringBuilder toYangFragment(final StringBuilder sb) {
+ return sb.append(prefix).append(':').append(identifier());
+ }
+ }
+
+ @Beta
+ public static final class Unqualified extends IRKeyword {
+ Unqualified(final String localName) {
+ super(localName);
+ }
+
+ @Override
+ public String prefix() {
+ return null;
+ }
+
+ @Override
+ public String asStringDeclaration() {
+ return identifier();
+ }
+
+ @Override
+ StringBuilder toYangFragment(final StringBuilder sb) {
+ return sb.append(identifier());
+ }
+ }
+
+ private final @NonNull String identifier;
+
+ IRKeyword(final String localName) {
+ this.identifier = requireNonNull(localName);
+ }
+
+ /**
+ * This keyword's 'identifier' part. This corresponds to what the RFCs refer to as {@code YANG keyword} or as
+ * {@code language extension keyword}.
+ *
+ * <p>
+ * Note the returned string is guaranteed to conform to rules of {@code identifier} ABNF and therefore
+ * is directly usable as a {@code localName} in an {@link AbstractQName}.
+ *
+ * @return This keyword's identifier part.
+ */
+ public final @NonNull String identifier() {
+ return identifier;
+ }
+
+ /**
+ * This keyword's 'prefix' part. This corresponds to {@code prefix identifier}. For {@code YANG keyword}s this is
+ * null. For language extension references this is the non-null prefix which references the YANG module defining
+ * the language extension.
+ *
+ * <p>
+ * Note the returned string, if non-null, is guaranteed to conform to rules of {@code identifier} ABNF and therefore
+ * is directly usable as a {@code localName} in an {@link AbstractQName}.
+ *
+ * @return This keyword's prefix, or null if this keyword references a YANG keyword.
+ */
+ public abstract @Nullable String prefix();
+
+ /**
+ * Helper method to re-create the string which was used to declared this keyword.
+ *
+ * @return Declaration string.
+ */
+ public abstract @NonNull String asStringDeclaration();
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.StatementContext;
+
+/**
+ * A single YANG statement in its raw string form. A statement is composed of:
+ * <ul>
+ * <li>a mandatory keyword, modeled as {@link IRKeyword}</li>
+ * <li>an optional argument, modeled as {@link IRArgument}</li>
+ * <li>zero or more nested statements</li>
+ * </ul>
+ */
+@Beta
+public abstract class IRStatement extends AbstractIRObject {
+ private final @NonNull IRKeyword keyword;
+ private final IRArgument argument;
+
+ IRStatement(final IRKeyword keyword, final IRArgument argument) {
+ this.keyword = requireNonNull(keyword);
+ this.argument = argument;
+ }
+
+ /**
+ * Create an {@link IRStatement} from a parsed {@link StatementContext}.
+ *
+ * @param context ANTLR statement context
+ * @return A new IRStatement
+ * @throws NullPointerException if {@code context} is null
+ */
+ public static @NonNull IRStatement forContext(final StatementContext context) {
+ return new StatementFactory().createStatement(context);
+ }
+
+ /**
+ * Return this statement's keyword.
+ *
+ * @return This statement's keyword.
+ */
+ public final @NonNull IRKeyword keyword() {
+ return keyword;
+ }
+
+ /**
+ * Return this statement's argument, if it is present.
+ *
+ * @return This statement's argument, or null if this statement does not have an argument
+ */
+ public final @Nullable IRArgument argument() {
+ return argument;
+ }
+
+ /**
+ * Return this statement's substatements.
+ *
+ * @return This statement's substatements.
+ */
+ public @NonNull List<? extends IRStatement> statements() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Return the line number on which this statement's keyword has its first character, counting from <b>1</b>. This
+ * information is used only for diagnostic purposes.
+ *
+ * @return Line number where this statement started in the source code.
+ */
+ public abstract int startLine();
+
+ /**
+ * Return the column number on which this statement's keyword has its first character, counting from <b>0</b>. This
+ * information is used only for diagnostic purposes.
+ *
+ * @return Column number where this statement started in the source code.
+ */
+ public abstract int startColumn();
+
+ @Override
+ final StringBuilder toYangFragment(final StringBuilder sb) {
+ sb.append(keyword);
+ if (argument != null) {
+ argument.toYangFragment(sb.append(' '));
+ }
+
+ final List<? extends IRStatement> statements = statements();
+ if (statements.isEmpty()) {
+ return sb.append(';');
+ }
+
+ sb.append(" {\n");
+ for (IRStatement stmt : statements) {
+ stmt.toYangFragment(sb).append('\n');
+ }
+ return sb.append('}');
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
+
+final class IRStatement022 extends IRStatement {
+ private final short startLine;
+ private final short startColumn;
+
+ IRStatement022(final IRKeyword keyword, final IRArgument argument, final int startLine, final int startColumn) {
+ super(keyword, argument);
+ this.startLine = (short) startLine;
+ this.startColumn = (short) startColumn;
+ }
+
+ @Override
+ public int startLine() {
+ return Short.toUnsignedInt(startLine);
+ }
+
+ @Override
+ public int startColumn() {
+ return Short.toUnsignedInt(startColumn);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
+
+final class IRStatement031 extends IRStatement {
+ private final int value;
+
+ IRStatement031(final IRKeyword keyword, final IRArgument argument, final int startLine, final int startColumn) {
+ super(keyword, argument);
+ this.value = startLine << 8 | startColumn & 0xFF;
+ }
+
+ @Override
+ public int startLine() {
+ return value >>> 8;
+ }
+
+ @Override
+ public int startColumn() {
+ return value & 0xFF;
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
+
+class IRStatement044 extends IRStatement {
+ private final int startLine;
+ private final int startColumn;
+
+ IRStatement044(final IRKeyword keyword, final IRArgument argument, final int startLine, final int startColumn) {
+ super(keyword, argument);
+ this.startLine = startLine;
+ this.startColumn = startColumn;
+ }
+
+ @Override
+ public final int startLine() {
+ return startLine;
+ }
+
+ @Override
+ public final int startColumn() {
+ return startColumn;
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import org.eclipse.jdt.annotation.NonNull;
+
+final class IRStatement144 extends IRStatement044 {
+ private final @NonNull IRStatement statement;
+
+ IRStatement144(final IRKeyword keyword, final IRArgument argument, final IRStatement statement,
+ final int startLine, final int startColumn) {
+ super(keyword, argument, startLine, startColumn);
+ this.statement = requireNonNull(statement);
+ }
+
+ @Override
+ public ImmutableList<IRStatement> statements() {
+ return ImmutableList.of(statement);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import org.eclipse.jdt.annotation.NonNull;
+
+final class IRStatementL44 extends IRStatement044 {
+ private final @NonNull ImmutableList<IRStatement> statements;
+
+ IRStatementL44(final IRKeyword keyword, final IRArgument argument, final ImmutableList<IRStatement> statements,
+ final int startLine, final int startColumn) {
+ super(keyword, argument, startLine, startColumn);
+ this.statements = requireNonNull(statements);
+ }
+
+ @Override
+ public ImmutableList<IRStatement> statements() {
+ return statements;
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
+
+import static com.google.common.base.Verify.verify;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.VerifyException;
+import com.google.common.collect.ImmutableList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.Function;
+import org.antlr.v4.runtime.Token;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.TerminalNode;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser;
+import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.ArgumentContext;
+import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.KeywordContext;
+import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.QuotedStringContext;
+import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.StatementContext;
+import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.UnquotedStringContext;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Concatenation;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.DoubleQuoted;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Identifier;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Single;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.SingleQuoted;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Unquoted;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRKeyword.Qualified;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRKeyword.Unqualified;
+
+final class StatementFactory {
+ private static final CharMatcher WHITESPACE_MATCHER = CharMatcher.whitespace();
+
+ private final Map<String, DoubleQuoted> dquotArguments = new HashMap<>();
+ private final Map<String, SingleQuoted> squotArguments = new HashMap<>();
+ private final Map<String, Unquoted> uquotArguments = new HashMap<>();
+ private final Map<String, Identifier> idenArguments = new HashMap<>();
+ private final Map<String, Unqualified> uqualKeywords = new HashMap<>();
+ private final Map<Entry<String, String>, Qualified> qualKeywords = new HashMap<>();
+ private final Map<String, String> strings = new HashMap<>();
+
+ @NonNull IRStatement createStatement(final StatementContext stmt) {
+ final ParseTree firstChild = stmt.getChild(0);
+ verify(firstChild instanceof KeywordContext, "Unexpected shape of %s", stmt);
+
+ final ParseTree keywordStart = firstChild.getChild(0);
+ verify(keywordStart instanceof TerminalNode, "Unexpected keyword start %s", keywordStart);
+ final Token keywordToken = ((TerminalNode) keywordStart).getSymbol();
+
+ final IRKeyword keyword;
+ switch (firstChild.getChildCount()) {
+ case 1:
+ keyword = uqualKeywords.computeIfAbsent(strOf(keywordToken), Unqualified::new);
+ break;
+ case 3:
+ keyword = qualKeywords.computeIfAbsent(Map.entry(strOf(keywordToken), strOf(firstChild.getChild(2))),
+ entry -> new Qualified(entry.getKey(), entry.getValue()));
+ break;
+ default:
+ throw new VerifyException("Unexpected keyword " + firstChild);
+ }
+
+ final IRArgument argument = createArgument(stmt);
+ final ImmutableList<IRStatement> statements = createStatements(stmt);
+ final int line = keywordToken.getLine();
+ final int column = keywordToken.getCharPositionInLine();
+
+ switch (statements.size()) {
+ case 0:
+ return createStatement(keyword, argument, line, column);
+ case 1:
+ return new IRStatement144(keyword, argument, statements.get(0), line, column);
+ default:
+ return new IRStatementL44(keyword, argument, statements, line, column);
+ }
+ }
+
+ private static @NonNull IRStatement createStatement(final IRKeyword keyword, final IRArgument argument,
+ final int line, final int column) {
+ if (line >= 0 && column >= 0) {
+ if (line <= 65535 && column <= 65535) {
+ return new IRStatement022(keyword, argument, line, column);
+ }
+ if (line <= 16777215 && column <= 255) {
+ return new IRStatement031(keyword, argument, line, column);
+ }
+ }
+ return new IRStatement044(keyword, argument, line, column);
+ }
+
+ private IRArgument createArgument(final StatementContext stmt) {
+ final ArgumentContext argCtx = stmt.argument();
+ if (argCtx == null) {
+ return null;
+ }
+ if (argCtx.getChildCount() == 1) {
+ final ParseTree child = argCtx.getChild(0);
+ if (child instanceof TerminalNode) {
+ // This is as simple as it gets: we are dealing with an identifier here.
+ return idenArguments.computeIfAbsent(strOf(((TerminalNode) child).getSymbol()), Identifier::new);
+ }
+ if (child instanceof UnquotedStringContext) {
+ // TODO: check non-presence of quotes and create a different subclass, so that ends up treated as if it
+ // was single-quoted, i.e. bypass the check implied by IRArgument.Single#needQuoteCheck().
+ return uquotArguments.computeIfAbsent(strOf(child), Unquoted::new);
+ }
+
+ verify(child instanceof QuotedStringContext, "Unexpected child %s", child);
+ return createArgument((QuotedStringContext) child);
+ }
+
+ // TODO: perform concatenation of single-quoted strings. For double-quoted strings this may not be as nice, but
+ // for single-quoted strings we do not need further validation in in the reactor and can use them as raw
+ // literals. This saves some indirection overhead (on memory side) and can slightly improve execution
+ // speed when we process the same IR multiple times.
+
+ return new Concatenation(argCtx.quotedString().stream().map(this::createArgument)
+ .collect(ImmutableList.toImmutableList()));
+ }
+
+ private Single createArgument(final QuotedStringContext argument) {
+ final ParseTree literal = argument.getChild(1);
+ verify(literal instanceof TerminalNode, "Unexpected literal %s", literal);
+ final Token token = ((TerminalNode) literal).getSymbol();
+ switch (token.getType()) {
+ case YangStatementParser.DQUOT_END:
+ return dquotArguments.computeIfAbsent("", DoubleQuoted::new);
+ case YangStatementParser.DQUOT_STRING:
+ // Whitespace normalization happens irrespective of further handling and has no effect on the result
+ final String str = intern(trimWhitespace(token.getText(), token.getCharPositionInLine() - 1));
+
+ // TODO: turn this into a single-quoted literal if a backslash is not present. Doing so allows the
+ // argument to be treated as a literal. See IRArgument.Single#needUnescape() for more context.
+ // This may look unimportant, but there are scenarios where we process the same AST multiple times
+ // and remembering this detail saves a string scan.
+
+ return dquotArguments.computeIfAbsent(str, DoubleQuoted::new);
+ case YangStatementParser.SQUOT_END:
+ return squotArguments.computeIfAbsent("", SingleQuoted::new);
+ case YangStatementParser.SQUOT_STRING:
+ return squotArguments.computeIfAbsent(strOf(token), SingleQuoted::new);
+ default:
+ throw new VerifyException("Unexpected token " + token);
+ }
+ }
+
+ private ImmutableList<IRStatement> createStatements(final StatementContext stmt) {
+ final List<StatementContext> statements = stmt.statement();
+ return statements.isEmpty() ? ImmutableList.of()
+ : statements.stream().map(this::createStatement).collect(ImmutableList.toImmutableList());
+ }
+
+ private String strOf(final ParseTree tree) {
+ return intern(tree.getText());
+ }
+
+ private String strOf(final Token token) {
+ return intern(token.getText());
+ }
+
+ private String intern(final String str) {
+ return strings.computeIfAbsent(str, Function.identity());
+ }
+
+ @VisibleForTesting
+ static String trimWhitespace(final String str, final int dquot) {
+ final int firstBrk = str.indexOf('\n');
+ if (firstBrk == -1) {
+ return str;
+ }
+
+ // Okay, we may need to do some trimming, set up a builder and append the first segment
+ final int length = str.length();
+ final StringBuilder sb = new StringBuilder(length);
+
+ // Append first segment, which needs only tail-trimming
+ sb.append(str, 0, trimTrailing(str, 0, firstBrk)).append('\n');
+
+ // With that out of the way, setup our iteration state. The string segment we are looking at is
+ // str.substring(start, end), which is guaranteed not to include any line breaks, i.e. end <= brk unless we are
+ // at the last segment.
+ int start = firstBrk + 1;
+ int brk = str.indexOf('\n', start);
+
+ // Loop over inner strings
+ while (brk != -1) {
+ trimLeadingAndAppend(sb, dquot, str, start, trimTrailing(str, start, brk)).append('\n');
+ start = brk + 1;
+ brk = str.indexOf('\n', start);
+ }
+
+ return trimLeadingAndAppend(sb, dquot, str, start, length).toString();
+ }
+
+ private static StringBuilder trimLeadingAndAppend(final StringBuilder sb, final int dquot, final String str,
+ final int start, final int end) {
+ int offset = start;
+ int pos = 0;
+
+ while (pos <= dquot) {
+ if (offset == end) {
+ // We ran out of data, nothing to append
+ return sb;
+ }
+
+ final char ch = str.charAt(offset);
+ if (ch == '\t') {
+ // tabs are to be treated as 8 spaces
+ pos += 8;
+ } else if (WHITESPACE_MATCHER.matches(ch)) {
+ pos++;
+ } else {
+ break;
+ }
+
+ offset++;
+ }
+
+ // We have expanded beyond double quotes, push equivalent spaces
+ while (pos - 1 > dquot) {
+ sb.append(' ');
+ pos--;
+ }
+
+ return sb.append(str, offset, end);
+ }
+
+ private static int trimTrailing(final String str, final int start, final int end) {
+ int ret = end;
+ while (ret > start) {
+ final int prev = ret - 1;
+ if (!WHITESPACE_MATCHER.matches(str.charAt(prev))) {
+ break;
+ }
+ ret = prev;
+ }
+ return ret;
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+/**
+ * Intermediate representation of a YANG file. This is an Abstract Syntax Tree equivalent to ParseTree we get from
+ * ANTLR, except it is immutable and has a denser in-memory representation due to it not containing any metadata which
+ * is not required for the purposes of statement inference.
+ *
+ * <p>
+ * The main entrypoint into this package is {@link org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRStatement}, which
+ * represents a single YANG statement. Every YANG file is required to contain exactly one top-level statement,
+ * {@code module} or {@code submodule}, hence an IRStatement also represents the significant contents of a YANG file.
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
\ No newline at end of file
import com.google.common.annotations.Beta;
import java.util.Optional;
-import org.antlr.v4.runtime.ParserRuleContext;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.yangtools.concepts.AbstractIdentifiable;
import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceRepresentation;
import org.opendaylight.yangtools.yang.model.repo.api.SemVerSourceIdentifier;
import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRStatement;
/**
* Abstract Syntax Tree representation of a schema source. This representation is internal to the YANG parser
implements SchemaSourceRepresentation {
private final @NonNull YangModelDependencyInfo depInfo;
private final @NonNull SemVerSourceIdentifier semVerId;
- private final @NonNull ParserRuleContext tree;
+ private final @NonNull IRStatement rootStatement;
private final @Nullable String symbolicName;
private ASTSchemaSource(final @NonNull SourceIdentifier identifier, final @NonNull SemVerSourceIdentifier semVerId,
- final @NonNull ParserRuleContext tree, final @NonNull YangModelDependencyInfo depInfo,
+ final @NonNull IRStatement tree, final @NonNull YangModelDependencyInfo depInfo,
@Nullable final String symbolicName) {
super(identifier);
this.depInfo = requireNonNull(depInfo);
- this.tree = requireNonNull(tree);
+ this.rootStatement = requireNonNull(tree);
this.semVerId = requireNonNull(semVerId);
this.symbolicName = symbolicName;
}
* if we fail to extract dependency information.
*/
static @NonNull ASTSchemaSource create(final @NonNull SourceIdentifier identifier,
- final @Nullable String symbolicName, final @NonNull ParserRuleContext tree)
+ final @Nullable String symbolicName, final @NonNull IRStatement rootStatement)
throws YangSyntaxErrorException {
- final YangModelDependencyInfo depInfo = YangModelDependencyInfo.fromAST(identifier, tree);
+ final YangModelDependencyInfo depInfo = YangModelDependencyInfo.parseAST(rootStatement, identifier);
final SourceIdentifier id = getSourceId(depInfo);
final SemVerSourceIdentifier semVerId;
semVerId = getSemVerSourceId(depInfo);
}
- return new ASTSchemaSource(id, semVerId, tree, depInfo, symbolicName);
+ return new ASTSchemaSource(id, semVerId, rootStatement, depInfo, symbolicName);
}
@Override
}
/**
- * Return the underlying abstract syntax tree.
+ * Return the root statement of this source.
*
- * @return Underlying AST.
+ * @return Root statement.
*/
- public @NonNull ParserRuleContext getAST() {
- return tree;
+ public @NonNull IRStatement getRootStatement() {
+ return rootStatement;
}
/**
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
-import com.google.common.base.VerifyException;
-import org.antlr.v4.runtime.Token;
-import org.antlr.v4.runtime.tree.ParseTree;
-import org.antlr.v4.runtime.tree.TerminalNode;
+import java.util.List;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.yangtools.yang.common.YangVersion;
-import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser;
-import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.ArgumentContext;
-import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.QuotedStringContext;
-import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.UnquotedStringContext;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Concatenation;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument.Single;
import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
import org.opendaylight.yangtools.yang.parser.spi.source.StatementSourceReference;
}
}
- private static final CharMatcher WHITESPACE_MATCHER = CharMatcher.whitespace();
-
private ArgumentContextUtils() {
// Hidden on purpose
}
* based on the grammar assumptions. While this is more verbose, it cuts out a number of unnecessary code,
* such as intermediate List allocation et al.
*/
- final @NonNull String stringFromStringContext(final ArgumentContext context, final StatementSourceReference ref) {
- // Get first child, which we fully expect to exist and be a lexer token
- final ParseTree firstChild = context.getChild(0);
- if (firstChild instanceof TerminalNode) {
- // Simplest of cases -- it is a simple IDENTIFIER, hence we do not need to validate anything else and can
- // just grab the string and run with it.
- return firstChild.getText();
- }
-
- if (firstChild instanceof UnquotedStringContext) {
- // Simple case, just grab the text, as ANTLR has done all the heavy lifting
- final String str = firstChild.getText();
- checkUnquoted(str, ref);
- return str;
- }
-
- verify(firstChild instanceof QuotedStringContext, "Unexpected shape of %s", context);
- if (context.getChildCount() == 1) {
- // No concatenation needed, special-case
- return unquoteString((QuotedStringContext) firstChild, ref);
+ final @NonNull String stringFromStringContext(final IRArgument argument, final StatementSourceReference ref) {
+ if (argument instanceof Single) {
+ final Single single = (Single) argument;
+ final String str = single.string();
+ if (single.needQuoteCheck()) {
+ checkUnquoted(str, ref);
+ }
+ return single.needUnescape() ? unescape(str, ref) : str;
}
- // Potentially-complex case of string quoting, escaping and concatenation.
- return concatStrings(context, ref);
+ verify(argument instanceof Concatenation, "Unexpected argument %s", argument);
+ return concatStrings(((Concatenation) argument).parts(), ref);
}
- private String unquoteString(final QuotedStringContext context, final StatementSourceReference ref) {
- final ParseTree secondChild = context.getChild(1);
- verify(secondChild instanceof TerminalNode, "Unexpected shape of %s", context);
- final Token secondToken = ((TerminalNode) secondChild).getSymbol();
- final int type = secondToken.getType();
- switch (type) {
- case YangStatementParser.DQUOT_END:
- case YangStatementParser.SQUOT_END:
- // We are missing actual body, hence this is an empty string
- return "";
- case YangStatementParser.SQUOT_STRING:
- return secondChild.getText();
- case YangStatementParser.DQUOT_STRING:
- // We should be looking at the first token, which is DQUOT_START, but since it is a single-character
- // token, let's not bother.
- return normalizeDoubleQuoted(secondChild.getText(), secondToken.getCharPositionInLine() - 1, ref);
- default:
- throw new VerifyException("Unhandled token type " + type);
- }
- }
-
- private String concatStrings(final ArgumentContext context, final StatementSourceReference ref) {
- /*
- * We have multiple fragments. Just search the tree. This code is equivalent to
- *
- * context.quotedString().forEach(stringNode -> sb.append(unquoteString(stringNode, ref))
- *
- * except we minimize allocations which that would do.
- */
+ private @NonNull String concatStrings(final List<? extends Single> parts, final StatementSourceReference ref) {
final StringBuilder sb = new StringBuilder();
- for (ParseTree child : context.children) {
- if (child instanceof TerminalNode) {
- final TerminalNode childNode = (TerminalNode) child;
- switch (childNode.getSymbol().getType()) {
- case YangStatementParser.SEP:
- case YangStatementParser.PLUS:
- // Operator, which we are handling by concat
- break;
- default:
- throw new VerifyException("Unexpected symbol in " + childNode);
- }
- } else {
- verify(child instanceof QuotedStringContext, "Unexpected fragment component %s", child);
- sb.append(unquoteString((QuotedStringContext) child, ref));
- }
+ for (Single part : parts) {
+ final String str = part.string();
+ sb.append(part.needUnescape() ? unescape(str, ref) : str);
}
return sb.toString();
}
- private String normalizeDoubleQuoted(final String str, final int dquot, final StatementSourceReference ref) {
- // Whitespace normalization happens irrespective of further handling and has no effect on the result
- final String stripped = trimWhitespace(str, dquot);
-
- // Now we need to perform some amount of unescaping. This serves as a pre-check before we dispatch
- // validation and processing (which will reuse the work we have done)
- final int backslash = stripped.indexOf('\\');
- return backslash == -1 ? stripped : unescape(ref, stripped, backslash);
- }
-
/*
* NOTE: Enforcement and transformation logic done by these methods should logically reside in the lexer and ANTLR
* account the for it with lexer modes. We do not want to force a re-lexing phase in the parser just because
abstract void checkUnquoted(String str, StatementSourceReference ref);
+ private @NonNull String unescape(final String str, final StatementSourceReference ref) {
+ // Now we need to perform some amount of unescaping. This serves as a pre-check before we dispatch
+ // validation and processing (which will reuse the work we have done)
+ final int backslash = str.indexOf('\\');
+ return backslash == -1 ? str : unescape(ref, str, backslash);
+ }
+
/*
* Unescape escaped double quotes, tabs, new line and backslash in the inner string and trim the result.
*/
- private String unescape(final StatementSourceReference ref, final String str, final int backslash) {
+ private @NonNull String unescape(final StatementSourceReference ref, final String str, final int backslash) {
checkDoubleQuoted(str, ref, backslash);
StringBuilder sb = new StringBuilder(str.length());
unescapeBackslash(sb, str, backslash);
sb.append(str, backslash, nextAfterBackslash + 1);
}
}
-
- @VisibleForTesting
- static String trimWhitespace(final String str, final int dquot) {
- final int firstBrk = str.indexOf('\n');
- if (firstBrk == -1) {
- return str;
- }
-
- // Okay, we may need to do some trimming, set up a builder and append the first segment
- final int length = str.length();
- final StringBuilder sb = new StringBuilder(length);
-
- // Append first segment, which needs only tail-trimming
- sb.append(str, 0, trimTrailing(str, 0, firstBrk)).append('\n');
-
- // With that out of the way, setup our iteration state. The string segment we are looking at is
- // str.substring(start, end), which is guaranteed not to include any line breaks, i.e. end <= brk unless we are
- // at the last segment.
- int start = firstBrk + 1;
- int brk = str.indexOf('\n', start);
-
- // Loop over inner strings
- while (brk != -1) {
- trimLeadingAndAppend(sb, dquot, str, start, trimTrailing(str, start, brk)).append('\n');
- start = brk + 1;
- brk = str.indexOf('\n', start);
- }
-
- return trimLeadingAndAppend(sb, dquot, str, start, length).toString();
- }
-
- private static StringBuilder trimLeadingAndAppend(final StringBuilder sb, final int dquot, final String str,
- final int start, final int end) {
- int offset = start;
- int pos = 0;
-
- while (pos <= dquot) {
- if (offset == end) {
- // We ran out of data, nothing to append
- return sb;
- }
-
- final char ch = str.charAt(offset);
- if (ch == '\t') {
- // tabs are to be treated as 8 spaces
- pos += 8;
- } else if (WHITESPACE_MATCHER.matches(ch)) {
- pos++;
- } else {
- break;
- }
-
- offset++;
- }
-
- // We have expanded beyond double quotes, push equivalent spaces
- while (pos - 1 > dquot) {
- sb.append(' ');
- pos--;
- }
-
- return sb.append(str, offset, end);
- }
-
- private static int trimTrailing(final String str, final int start, final int end) {
- int ret = end;
- while (ret > start) {
- final int prev = ret - 1;
- if (!WHITESPACE_MATCHER.matches(str.charAt(prev))) {
- break;
- }
- ret = prev;
- }
- return ret;
- }
}
*/
package org.opendaylight.yangtools.yang.parser.rfc7950.repo;
-import static com.google.common.base.Verify.verifyNotNull;
import static java.util.Objects.requireNonNull;
-import com.google.common.base.VerifyException;
import java.util.Optional;
-import org.antlr.v4.runtime.Token;
-import org.antlr.v4.runtime.tree.ParseTree;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.common.QNameModule;
import org.opendaylight.yangtools.yang.common.YangConstants;
import org.opendaylight.yangtools.yang.common.YangVersion;
import org.opendaylight.yangtools.yang.model.api.meta.StatementDefinition;
-import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.ArgumentContext;
-import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.KeywordContext;
-import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.StatementContext;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRKeyword;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRKeyword.Qualified;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRStatement;
import org.opendaylight.yangtools.yang.parser.spi.source.DeclarationInTextSource;
import org.opendaylight.yangtools.yang.parser.spi.source.PrefixToModule;
import org.opendaylight.yangtools.yang.parser.spi.source.QNameToStatementDefinition;
this.prefixes = prefixes;
}
- void visit(final StatementContext context) {
- processStatement(0, context);
+ void visit(final IRStatement stmt) {
+ processStatement(0, stmt);
}
/**
* @param ref Source reference
* @return valid QName for declared statement to be written, or null
*/
- QName getValidStatementDefinition(final KeywordContext keyword, final StatementSourceReference ref) {
- switch (keyword.getChildCount()) {
- case 1:
- final StatementDefinition def = stmtDef.get(QName.create(YangConstants.RFC6020_YIN_MODULE,
- keyword.getChild(0).getText()));
- return def != null ? def.getStatementName() : null;
- case 3:
- if (prefixes == null) {
- // No prefixes to look up from
- return null;
- }
+ QName getValidStatementDefinition(final IRKeyword keyword, final StatementSourceReference ref) {
+ if (keyword instanceof Qualified) {
+ return getValidStatementDefinition((Qualified) keyword, ref);
+ }
+ final StatementDefinition def = stmtDef.get(QName.create(YangConstants.RFC6020_YIN_MODULE,
+ keyword.identifier()));
+ return def != null ? def.getStatementName() : null;
+ }
- final String prefix = keyword.getChild(0).getText();
- final QNameModule qNameModule = prefixes.get(prefix);
- if (qNameModule == null) {
- // Failed to look the namespace
- return null;
- }
+ private QName getValidStatementDefinition(final Qualified keyword, final StatementSourceReference ref) {
+ if (prefixes == null) {
+ // No prefixes to look up from
+ return null;
+ }
- final String localName = keyword.getChild(2).getText();
- final StatementDefinition foundStmtDef = resolveStatement(qNameModule, localName);
- return foundStmtDef != null ? foundStmtDef.getStatementName() : null;
- default:
- throw new VerifyException("Unexpected shape of " + keyword);
+ final QNameModule qNameModule = prefixes.get(keyword.prefix());
+ if (qNameModule == null) {
+ // Failed to look the namespace
+ return null;
}
+
+ final StatementDefinition foundStmtDef = resolveStatement(qNameModule, keyword.identifier());
+ return foundStmtDef != null ? foundStmtDef.getStatementName() : null;
}
StatementDefinition resolveStatement(final QNameModule module, final String localName) {
}
// Normal entry point, checks for potential resume
- private boolean processStatement(final int myOffset, final StatementContext ctx) {
+ private boolean processStatement(final int myOffset, final IRStatement stmt) {
final Optional<? extends ResumedStatement> optResumed = writer.resumeStatement(myOffset);
if (optResumed.isPresent()) {
final ResumedStatement resumed = optResumed.get();
- return resumed.isFullyDefined() || doProcessStatement(ctx, resumed.getSourceReference());
+ return resumed.isFullyDefined() || doProcessStatement(stmt, resumed.getSourceReference());
}
- return processNewStatement(myOffset, ctx);
+ return processNewStatement(myOffset, stmt);
}
// Slow-path allocation of a new statement
- private boolean processNewStatement(final int myOffset, final StatementContext ctx) {
- final Token start = ctx.getStart();
- final StatementSourceReference ref = DeclarationInTextSource.atPosition(sourceName, start.getLine(),
- start.getCharPositionInLine());
- final QName def = getValidStatementDefinition(verifyNotNull(ctx.getChild(KeywordContext.class, 0)), ref);
+ private boolean processNewStatement(final int myOffset, final IRStatement stmt) {
+ final StatementSourceReference ref = DeclarationInTextSource.atPosition(sourceName, stmt.startLine(),
+ stmt.startColumn());
+ final QName def = getValidStatementDefinition(stmt.keyword(), ref);
if (def == null) {
return false;
}
- final ArgumentContext argumentCtx = ctx.getChild(ArgumentContext.class, 0);
+ final IRArgument argumentCtx = stmt.argument();
final String argument = argumentCtx == null ? null : utils.stringFromStringContext(argumentCtx, ref);
writer.startStatement(myOffset, def, argument, ref);
- return doProcessStatement(ctx, ref);
+ return doProcessStatement(stmt, ref);
}
// Actual processing
- private boolean doProcessStatement(final StatementContext ctx, final StatementSourceReference ref) {
+ private boolean doProcessStatement(final IRStatement stmt, final StatementSourceReference ref) {
int childOffset = 0;
boolean fullyDefined = true;
- if (ctx.children != null) {
- for (ParseTree s : ctx.children) {
- if (s instanceof StatementContext && !processStatement(childOffset++, (StatementContext) s)) {
- fullyDefined = false;
- }
+ for (IRStatement substatement : stmt.statements()) {
+ if (!processStatement(childOffset++, substatement)) {
+ fullyDefined = false;
}
}
// TODO: missing validation (YangModelBasicValidationListener should be re-implemented to new parser)
- return ASTSchemaSource.create(text.getIdentifier(), text.getSymbolicName().orElse(null),
- src.statementContext());
+ return ASTSchemaSource.create(text.getIdentifier(), text.getSymbolicName().orElse(null), src.rootStatement());
}
}
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.HashSet;
-import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
-import org.antlr.v4.runtime.ParserRuleContext;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.yangtools.concepts.SemVer;
import org.opendaylight.yangtools.yang.model.parser.api.YangSyntaxErrorException;
import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
-import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.ArgumentContext;
-import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.StatementContext;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRArgument;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRKeyword;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRKeyword.Unqualified;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRStatement;
import org.opendaylight.yangtools.yang.parser.spi.source.DeclarationInTextSource;
import org.opendaylight.yangtools.yang.parser.spi.source.StatementSourceReference;
* Extracts {@link YangModelDependencyInfo} from an abstract syntax tree of a YANG model.
*
* @param source Source identifier
- * @param tree Abstract syntax tree
+ * @param rootStatement root statement
* @return {@link YangModelDependencyInfo}
- * @throws YangSyntaxErrorException If the AST is not a valid YANG module/submodule
+ * @throws IllegalArgumentException If the AST is not a valid YANG module/submodule
*/
- static @NonNull YangModelDependencyInfo fromAST(final SourceIdentifier source, final ParserRuleContext tree)
- throws YangSyntaxErrorException {
-
- if (tree instanceof StatementContext) {
- final StatementContext rootStatement = (StatementContext) tree;
- return parseAST(rootStatement, source);
- }
-
- throw new YangSyntaxErrorException(source, 0, 0, "Unknown YANG text type");
- }
-
- private static @NonNull YangModelDependencyInfo parseAST(final StatementContext rootStatement,
+ static @NonNull YangModelDependencyInfo parseAST(final IRStatement rootStatement,
final SourceIdentifier source) {
- final String keyWordText = rootStatement.keyword().getText();
- if (MODULE.equals(keyWordText)) {
+ final IRKeyword keyword = rootStatement.keyword();
+ checkArgument(keyword instanceof Unqualified, "Invalid root statement %s", keyword);
+
+ final String arg = keyword.identifier();
+ if (MODULE.equals(arg)) {
return parseModuleContext(rootStatement, source);
}
- if (SUBMODULE.equals(keyWordText)) {
+ if (SUBMODULE.equals(arg)) {
return parseSubmoduleContext(rootStatement, source);
}
throw new IllegalArgumentException("Root of parsed AST must be either module or submodule");
throws IOException, YangSyntaxErrorException {
final YangStatementStreamSource source = YangStatementStreamSource.create(
YangTextSchemaSource.forResource(refClass, resourceName));
- return parseAST(source.statementContext(), source.getIdentifier());
+ return parseAST(source.rootStatement(), source.getIdentifier());
}
- private static @NonNull YangModelDependencyInfo parseModuleContext(final StatementContext module,
+ private static @NonNull YangModelDependencyInfo parseModuleContext(final IRStatement module,
final SourceIdentifier source) {
final String name = safeStringArgument(source, module, "module name");
final String latestRevision = getLatestRevision(module, source);
return new ModuleDependencyInfo(name, latestRevision, imports, includes, semVer);
}
- private static ImmutableSet<ModuleImport> parseImports(final StatementContext module,
+ private static ImmutableSet<ModuleImport> parseImports(final IRStatement module,
final SourceIdentifier source) {
final Set<ModuleImport> result = new HashSet<>();
- for (final StatementContext subStatementContext : module.statement()) {
- if (IMPORT.equals(subStatementContext.keyword().getText())) {
- final String importedModuleName = safeStringArgument(source, subStatementContext,
- "imported module name");
- final String revisionDateStr = getRevisionDateString(subStatementContext, source);
+ for (final IRStatement substatement : module.statements()) {
+ if (isBuiltin(substatement, IMPORT)) {
+ final String importedModuleName = safeStringArgument(source, substatement, "imported module name");
+ final String revisionDateStr = getRevisionDateString(substatement, source);
final Revision revisionDate = Revision.ofNullable(revisionDateStr).orElse(null);
- final SemVer importSemVer = findSemanticVersion(subStatementContext, source);
+ final SemVer importSemVer = findSemanticVersion(substatement, source);
result.add(new ModuleImportImpl(importedModuleName, revisionDate, importSemVer));
}
}
return ImmutableSet.copyOf(result);
}
- private static SemVer findSemanticVersion(final StatementContext statement, final SourceIdentifier source) {
+ private static SemVer findSemanticVersion(final IRStatement statement, final SourceIdentifier source) {
String semVerString = null;
- for (final StatementContext subStatement : statement.statement()) {
- final String subStatementName = trimPrefix(subStatement.keyword().getText());
- if (OPENCONFIG_VERSION.equals(subStatementName)) {
- semVerString = safeStringArgument(source, subStatement, "version string");
+ for (final IRStatement substatement : statement.statements()) {
+ // FIXME: this should also check we are using a prefix
+ if (OPENCONFIG_VERSION.equals(substatement.keyword().identifier())) {
+ semVerString = safeStringArgument(source, substatement, "version string");
break;
}
}
return Strings.isNullOrEmpty(semVerString) ? null : SemVer.valueOf(semVerString);
}
-
- private static String trimPrefix(final String identifier) {
- final List<String> namesParts = COLON_SPLITTER.splitToList(identifier);
- if (namesParts.size() == 2) {
- return namesParts.get(1);
- }
- return identifier;
+ private static boolean isBuiltin(final IRStatement stmt, final String localName) {
+ final IRKeyword keyword = stmt.keyword();
+ return keyword instanceof Unqualified && localName.equals(keyword.identifier());
}
-
- private static ImmutableSet<ModuleImport> parseIncludes(final StatementContext module,
- final SourceIdentifier source) {
+ private static ImmutableSet<ModuleImport> parseIncludes(final IRStatement module, final SourceIdentifier source) {
final Set<ModuleImport> result = new HashSet<>();
- for (final StatementContext subStatementContext : module.statement()) {
- if (INCLUDE.equals(subStatementContext.keyword().getText())) {
- final String revisionDateStr = getRevisionDateString(subStatementContext, source);
- final String IncludeModuleName = safeStringArgument(source, subStatementContext,
- "included submodule name");
+ for (final IRStatement substatement : module.statements()) {
+ if (isBuiltin(substatement, INCLUDE)) {
+ final String revisionDateStr = getRevisionDateString(substatement, source);
+ final String IncludeModuleName = safeStringArgument(source, substatement, "included submodule name");
final Revision revisionDate = Revision.ofNullable(revisionDateStr).orElse(null);
result.add(new ModuleImportImpl(IncludeModuleName, revisionDate));
}
return ImmutableSet.copyOf(result);
}
- private static String getRevisionDateString(final StatementContext importStatement, final SourceIdentifier source) {
+ private static String getRevisionDateString(final IRStatement importStatement, final SourceIdentifier source) {
String revisionDateStr = null;
- for (final StatementContext importSubStatement : importStatement.statement()) {
- if (REVISION_DATE.equals(importSubStatement.keyword().getText())) {
- revisionDateStr = safeStringArgument(source, importSubStatement, "imported module revision-date");
+ for (final IRStatement substatement : importStatement.statements()) {
+ if (isBuiltin(substatement, REVISION_DATE)) {
+ revisionDateStr = safeStringArgument(source, substatement, "imported module revision-date");
}
}
return revisionDateStr;
}
- public static String getLatestRevision(final StatementContext module, final SourceIdentifier source) {
+ public static String getLatestRevision(final IRStatement module, final SourceIdentifier source) {
String latestRevision = null;
- for (final StatementContext subStatementContext : module.statement()) {
- if (REVISION.equals(subStatementContext.keyword().getText())) {
- final String currentRevision = safeStringArgument(source, subStatementContext, "revision date");
+ for (final IRStatement substatement : module.statements()) {
+ if (isBuiltin(substatement, REVISION)) {
+ final String currentRevision = safeStringArgument(source, substatement, "revision date");
if (latestRevision == null || latestRevision.compareTo(currentRevision) < 0) {
latestRevision = currentRevision;
}
return latestRevision;
}
- private static @NonNull YangModelDependencyInfo parseSubmoduleContext(final StatementContext submodule,
+ private static @NonNull YangModelDependencyInfo parseSubmoduleContext(final IRStatement submodule,
final SourceIdentifier source) {
final String name = safeStringArgument(source, submodule, "submodule name");
final String belongsTo = parseBelongsTo(submodule, source);
return new SubmoduleDependencyInfo(name, latestRevision, belongsTo, imports, includes);
}
- private static String parseBelongsTo(final StatementContext submodule, final SourceIdentifier source) {
- for (final StatementContext subStatementContext : submodule.statement()) {
- if (BELONGS_TO.equals(subStatementContext.keyword().getText())) {
- return safeStringArgument(source, subStatementContext, "belongs-to module name");
+ private static String parseBelongsTo(final IRStatement submodule, final SourceIdentifier source) {
+ for (final IRStatement substatement : submodule.statements()) {
+ if (isBuiltin(substatement, BELONGS_TO)) {
+ return safeStringArgument(source, substatement, "belongs-to module name");
}
}
return null;
}
- private static String safeStringArgument(final SourceIdentifier source, final StatementContext stmt,
- final String desc) {
+ private static String safeStringArgument(final SourceIdentifier source, final IRStatement stmt, final String desc) {
final StatementSourceReference ref = getReference(source, stmt);
- final ArgumentContext arg = stmt.argument();
+ final IRArgument arg = stmt.argument();
checkArgument(arg != null, "Missing %s at %s", desc, ref);
// TODO: we probably need to understand yang version first....
return ArgumentContextUtils.rfc6020().stringFromStringContext(arg, ref);
}
- private static StatementSourceReference getReference(final SourceIdentifier source,
- final StatementContext context) {
- return DeclarationInTextSource.atPosition(source.getName(), context.getStart().getLine(),
- context.getStart().getCharPositionInLine());
+ private static StatementSourceReference getReference(final SourceIdentifier source, final IRStatement stmt) {
+ return DeclarationInTextSource.atPosition(source.getName(), stmt.startLine(), stmt.startColumn());
}
/**
*/
package org.opendaylight.yangtools.yang.parser.rfc7950.repo;
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Verify.verifyNotNull;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.Beta;
-import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.io.InputStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
-import org.antlr.v4.runtime.ParserRuleContext;
-import org.antlr.v4.runtime.tree.ErrorNode;
-import org.antlr.v4.runtime.tree.ParseTreeListener;
-import org.antlr.v4.runtime.tree.ParseTreeWalker;
-import org.antlr.v4.runtime.tree.TerminalNode;
import org.opendaylight.yangtools.concepts.AbstractIdentifiable;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.common.QNameModule;
import org.opendaylight.yangtools.yang.parser.antlr.YangStatementLexer;
import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser;
import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.FileContext;
-import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.KeywordContext;
-import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.StatementContext;
import org.opendaylight.yangtools.yang.parser.rfc7950.antlr.CompactYangStatementLexer;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRKeyword;
+import org.opendaylight.yangtools.yang.parser.rfc7950.ir.IRStatement;
import org.opendaylight.yangtools.yang.parser.spi.source.PrefixToModule;
import org.opendaylight.yangtools.yang.parser.spi.source.QNameToStatementDefinition;
import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
@Beta
public final class YangStatementStreamSource extends AbstractIdentifiable<SourceIdentifier>
implements StatementStreamSource {
- private static final ParseTreeListener MAKE_IMMUTABLE_LISTENER = new ParseTreeListener() {
- @Override
- public void enterEveryRule(final ParserRuleContext ctx) {
- // No-op
- }
-
- @Override
- public void exitEveryRule(final ParserRuleContext ctx) {
- ctx.children = ctx.children == null ? ImmutableList.of() : ImmutableList.copyOf(ctx.children);
- }
-
- @Override
- public void visitTerminal(final TerminalNode node) {
- // No-op
- }
-
- @Override
- public void visitErrorNode(final ErrorNode node) {
- // No-op
- }
- };
-
- private final StatementContext context;
+ private final IRStatement rootStatement;
private final String sourceName;
- private YangStatementStreamSource(final SourceIdentifier identifier, final StatementContext context,
+ private YangStatementStreamSource(final SourceIdentifier identifier, final IRStatement rootStatement,
final String sourceName) {
super(identifier);
- this.context = requireNonNull(context);
+ this.rootStatement = requireNonNull(rootStatement);
this.sourceName = sourceName;
}
*/
public static YangStatementStreamSource create(final YangTextSchemaSource source) throws IOException,
YangSyntaxErrorException {
- final StatementContext context;
+ final IRStatement rootStatement;
try (InputStream stream = source.openStream()) {
- context = parseYangSource(source.getIdentifier(), stream);
+ rootStatement = parseYangSource(source.getIdentifier(), stream);
}
- return new YangStatementStreamSource(source.getIdentifier(), context, source.getSymbolicName().orElse(null));
+ return new YangStatementStreamSource(source.getIdentifier(), rootStatement,
+ source.getSymbolicName().orElse(null));
}
/**
* @return A new {@link YangStatementStreamSource}
*/
public static YangStatementStreamSource create(final ASTSchemaSource source) {
- final ParserRuleContext ast = source.getAST();
- checkArgument(ast instanceof StatementContext,
- "Unsupported context class %s for source %s", ast.getClass(), source.getIdentifier());
- return create(source.getIdentifier(), (StatementContext) ast, source.getSymbolicName().orElse(null));
+ return create(source.getIdentifier(), source.getRootStatement(), source.getSymbolicName().orElse(null));
}
- public static YangStatementStreamSource create(final SourceIdentifier identifier, final StatementContext context,
- final String symbolicName) {
- return new YangStatementStreamSource(identifier, context, symbolicName);
+ public static YangStatementStreamSource create(final SourceIdentifier identifier, final IRStatement rootStatement,
+ final String symbolicName) {
+ return new YangStatementStreamSource(identifier, rootStatement, symbolicName);
}
@Override
public void writePreLinkage(final StatementWriter writer, final QNameToStatementDefinition stmtDef) {
- new StatementContextVisitor(sourceName, writer, stmtDef, null, YangVersion.VERSION_1).visit(context);
+ new StatementContextVisitor(sourceName, writer, stmtDef, null, YangVersion.VERSION_1).visit(rootStatement);
}
@Override
StatementDefinition resolveStatement(final QNameModule module, final String localName) {
return stmtDef.getByNamespaceAndLocalName(module.getNamespace(), localName);
}
- }.visit(context);
+ }.visit(rootStatement);
}
@Override
public void writeLinkageAndStatementDefinitions(final StatementWriter writer,
final QNameToStatementDefinition stmtDef, final PrefixToModule prefixes, final YangVersion yangVersion) {
- new StatementContextVisitor(sourceName, writer, stmtDef, prefixes, yangVersion).visit(context);
+ new StatementContextVisitor(sourceName, writer, stmtDef, prefixes, yangVersion).visit(rootStatement);
}
@Override
final PrefixToModule prefixes, final YangVersion yangVersion) {
new StatementContextVisitor(sourceName, writer, stmtDef, prefixes, yangVersion) {
@Override
- QName getValidStatementDefinition(final KeywordContext keyword, final StatementSourceReference ref) {
+ QName getValidStatementDefinition(final IRKeyword keyword, final StatementSourceReference ref) {
final QName ret = super.getValidStatementDefinition(keyword, ref);
if (ret == null) {
throw new SourceException(ref, "%s is not a YANG statement or use of extension.",
- keyword.getText());
+ keyword.asStringDeclaration());
}
return ret;
}
- }.visit(context);
+ }.visit(rootStatement);
}
- StatementContext statementContext() {
- return context;
+ IRStatement rootStatement() {
+ return rootStatement;
}
- private static StatementContext parseYangSource(final SourceIdentifier source, final InputStream stream)
+ private static IRStatement parseYangSource(final SourceIdentifier source, final InputStream stream)
throws IOException, YangSyntaxErrorException {
final YangStatementLexer lexer = new CompactYangStatementLexer(CharStreams.fromStream(stream));
final YangStatementParser parser = new YangStatementParser(new CommonTokenStream(lexer));
final FileContext result = parser.file();
errorListener.validate();
-
- // Walk the resulting tree and replace each children with an immutable list, lowering memory requirements
- // and making sure the resulting tree will not get accidentally modified. An alternative would be to use
- // org.antlr.v4.runtime.Parser.TrimToSizeListener, but that does not make the tree immutable.
- ParseTreeWalker.DEFAULT.walk(MAKE_IMMUTABLE_LISTENER, result);
-
- return verifyNotNull(result.statement());
+ return IRStatement.forContext(result.statement());
}
}
--- /dev/null
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.ir;
+
+import static org.junit.Assert.assertEquals;
+import static org.opendaylight.yangtools.yang.parser.rfc7950.ir.StatementFactory.trimWhitespace;
+
+import org.junit.Test;
+
+public class StatementFactoryTest {
+ @Test
+ public void testTrimWhitespace() {
+ assertEquals("\n", trimWhitespace("\n", 0));
+ assertEquals("\n", trimWhitespace("\n", 5));
+ assertEquals("\n\n\n\n", trimWhitespace("\n\n\n\n", 0));
+ assertEquals("\n\n\n\n", trimWhitespace("\n\n\n\n", 5));
+ assertEquals("abc\n\n", trimWhitespace("abc \n \n", 0));
+ assertEquals("abc\n\n", trimWhitespace("abc \n \n", 1));
+ assertEquals("abc\n ", trimWhitespace("abc\n ", 0));
+ assertEquals("abc\n", trimWhitespace("abc\n ", 2));
+ assertEquals("abc\n\n", trimWhitespace("abc\n \n", 2));
+ assertEquals("abc\n ", trimWhitespace("abc\n\t ", 0));
+ assertEquals("abc\n ", trimWhitespace("abc\n\t ", 2));
+ assertEquals("abc\n ", trimWhitespace("abc\n\t ", 4));
+ assertEquals("abc\n ", trimWhitespace("abc\n \t", 4));
+ assertEquals("abc\n a\n a\n", trimWhitespace("abc\n\ta\n\t a\n", 4));
+ assertEquals("abc\n\n a\n", trimWhitespace("abc\n\t\n\t a\n", 4));
+ assertEquals(" \ta\n", trimWhitespace(" \ta\n", 3));
+ assertEquals(" \ta\n", trimWhitespace(" \ta\n ", 3));
+ assertEquals(" \ta\n", trimWhitespace(" \ta\n ", 3));
+ assertEquals(" \ta\n", trimWhitespace(" \ta\n ", 3));
+ assertEquals(" \ta\n ", trimWhitespace(" \ta\n ", 3));
+ }
+}
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import static org.opendaylight.yangtools.yang.parser.rfc7950.repo.ArgumentContextUtils.trimWhitespace;
import static org.opendaylight.yangtools.yang.parser.rfc7950.repo.ArgumentContextUtils.unescapeBackslash;
import java.io.File;
import org.opendaylight.yangtools.yang.stmt.StmtTestUtils;
public class ArgumentContextUtilsTest {
- @Test
- public void testTrimWhitespace() {
- assertEquals("\n", trimWhitespace("\n", 0));
- assertEquals("\n", trimWhitespace("\n", 5));
- assertEquals("\n\n\n\n", trimWhitespace("\n\n\n\n", 0));
- assertEquals("\n\n\n\n", trimWhitespace("\n\n\n\n", 5));
- assertEquals("abc\n\n", trimWhitespace("abc \n \n", 0));
- assertEquals("abc\n\n", trimWhitespace("abc \n \n", 1));
- assertEquals("abc\n ", trimWhitespace("abc\n ", 0));
- assertEquals("abc\n", trimWhitespace("abc\n ", 2));
- assertEquals("abc\n\n", trimWhitespace("abc\n \n", 2));
- assertEquals("abc\n ", trimWhitespace("abc\n\t ", 0));
- assertEquals("abc\n ", trimWhitespace("abc\n\t ", 2));
- assertEquals("abc\n ", trimWhitespace("abc\n\t ", 4));
- assertEquals("abc\n ", trimWhitespace("abc\n \t", 4));
- assertEquals("abc\n a\n a\n", trimWhitespace("abc\n\ta\n\t a\n", 4));
- assertEquals("abc\n\n a\n", trimWhitespace("abc\n\t\n\t a\n", 4));
- assertEquals(" \ta\n", trimWhitespace(" \ta\n", 3));
- assertEquals(" \ta\n", trimWhitespace(" \ta\n ", 3));
- assertEquals(" \ta\n", trimWhitespace(" \ta\n ", 3));
- assertEquals(" \ta\n", trimWhitespace(" \ta\n ", 3));
- assertEquals(" \ta\n ", trimWhitespace(" \ta\n ", 3));
- }
-
@Test
public void testUnescapeNew() {
// a\b -----> a\b (invalid for 7950)