From 7dd5f8f77e14c58a9cda23dc837107c6b70371b9 Mon Sep 17 00:00:00 2001 From: Ben Bader Date: Sun, 10 Jun 2018 16:16:06 -0700 Subject: [PATCH] Convert thrift parser to Kotlin (#175) --- build.gradle | 23 +- gradle/gradle-mvn-push.gradle | 40 + .../microsoft.thrifty.gen/ConstantBuilder.kt | 47 +- .../ThriftyCodeGenerator.kt | 6 +- thrifty-schema/Module.md | 9 + thrifty-schema/build.gradle | 15 +- .../thrifty/schema/ErrorReporter.java | 46 +- .../com/microsoft/thrifty/schema/Linker.java | 2 +- .../com/microsoft/thrifty/schema/Loader.java | 3 +- .../schema/parser/AnnotationElement.java | 56 - .../thrifty/schema/parser/ConstElement.java | 57 - .../schema/parser/ConstValueElement.java | 149 -- .../thrifty/schema/parser/EnumElement.java | 61 - .../schema/parser/EnumMemberElement.java | 59 - .../thrifty/schema/parser/FieldElement.java | 67 - .../schema/parser/FunctionElement.java | 70 - .../thrifty/schema/parser/IncludeElement.java | 37 - .../schema/parser/ListTypeElement.java | 40 - .../thrifty/schema/parser/MapTypeElement.java | 42 - .../schema/parser/NamespaceElement.java | 55 - .../schema/parser/ScalarTypeElement.java | 37 - .../thrifty/schema/parser/ServiceElement.java | 64 - .../thrifty/schema/parser/SetTypeElement.java | 40 - .../thrifty/schema/parser/StructElement.java | 73 - .../schema/parser/ThriftFileElement.java | 73 - .../thrifty/schema/parser/ThriftListener.java | 831 ----------- .../thrifty/schema/parser/ThriftParser.java | 80 -- .../schema/parser/ThriftyParserPlugins.java | 72 - .../thrifty/schema/parser/TypeElement.java | 59 - .../thrifty/schema/parser/TypedefElement.java | 61 - .../thrifty/schema/parser/package-info.java | 28 - .../schema/parser/ConstValueElement.kt | 185 +++ .../thrifty/schema/parser/ParserTypes.kt | 677 +++++++++ .../thrifty/schema/parser/ThriftListener.kt | 781 +++++++++++ .../thrifty/schema/parser/ThriftParser.kt | 65 + .../schema/parser/ThriftyParserPlugins.kt | 89 ++ .../thrifty/schema/parser/TypeElements.kt | 250 ++++ .../thrifty/schema/ConstantTest.java | 12 +- .../thrifty/schema/EnumTypeTest.java | 17 +- .../microsoft/thrifty/schema/FieldTest.java | 87 +- .../schema/parser/ThriftParserTest.java | 1238 ----------------- .../thrifty/schema/parser/ThriftParserTest.kt | 1236 ++++++++++++++++ 42 files changed, 3475 insertions(+), 3464 deletions(-) create mode 100644 thrifty-schema/Module.md delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/AnnotationElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ConstElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ConstValueElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/EnumElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/EnumMemberElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/FieldElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/FunctionElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/IncludeElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ListTypeElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/MapTypeElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/NamespaceElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ScalarTypeElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ServiceElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/SetTypeElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/StructElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftFileElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftListener.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftParser.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftyParserPlugins.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/TypeElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/TypedefElement.java delete mode 100644 thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/package-info.java create mode 100644 thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ConstValueElement.kt create mode 100644 thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ParserTypes.kt create mode 100644 thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ThriftListener.kt create mode 100644 thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ThriftParser.kt create mode 100644 thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ThriftyParserPlugins.kt create mode 100644 thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/TypeElements.kt delete mode 100644 thrifty-schema/src/test/java/com/microsoft/thrifty/schema/parser/ThriftParserTest.java create mode 100644 thrifty-schema/src/test/kotlin/com/microsoft/thrifty/schema/parser/ThriftParserTest.kt diff --git a/build.gradle b/build.gradle index 7c4f809..4b632ef 100644 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,7 @@ buildscript { dependencies { classpath 'net.ltgt.gradle:gradle-errorprone-plugin:0.0.14' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.2.41" + classpath 'org.jetbrains.dokka:dokka-gradle-plugin:0.9.17' } } @@ -102,22 +103,6 @@ subprojects { sp -> options.incremental = true } - task javadocJar(type: Jar, dependsOn: javadoc) { - from 'build/docs/javadoc' - classifier = 'javadoc' - } - - task sourcesJar(type: Jar) { - from sourceSets.main.allSource - classifier = 'sources' - } - - artifacts { - archives jar - archives javadocJar - archives sourcesJar - } - test { testLogging { events "failed" @@ -139,10 +124,10 @@ task codeCoverageReport(type: JacocoReport) { } reports { - xml.enabled true + xml.enabled = true xml.destination file("$buildDir/reports/jacoco/report.xml") - html.enabled true - csv.enabled false + html.enabled = true + csv.enabled = false } afterEvaluate { diff --git a/gradle/gradle-mvn-push.gradle b/gradle/gradle-mvn-push.gradle index 9f53428..6ebf677 100644 --- a/gradle/gradle-mvn-push.gradle +++ b/gradle/gradle-mvn-push.gradle @@ -82,4 +82,44 @@ afterEvaluate { project -> required { isRelease && gradle.taskGraph.hasTask("uploadArchives") } sign configurations.archives } + + if (project.plugins.hasPlugin('org.jetbrains.kotlin') || project.plugins.hasPlugin('kotlin')) { + apply plugin: 'org.jetbrains.dokka' + + dokka { + includes = ['Module.md'] + + linkMapping { + dir = project.file("src/main/kotlin") + url = "https://github.com/Microsoft/thrifty/tree/master/${project.name}/src/main/kotlin" + suffix = "#L" + } + + outputFormat = 'html' + outputDirectory = "${project.buildDir}/docs/kdoc" + } + } + + task javadocJar(type: Jar) { + if (project.plugins.hasPlugin('org.jetbrains.dokka')) { + dependsOn dokka + from dokka.outputDirectory + } else { + dependsOn javadoc + from 'build/docs/javadoc' + } + + classifier = 'javadoc' + } + + task sourcesJar(type: Jar) { + from sourceSets.main.allSource + classifier = 'sources' + } + + artifacts { + archives jar + archives javadocJar + archives sourcesJar + } } diff --git a/thrifty-java-codegen/src/main/kotlin/com/microsoft.thrifty.gen/ConstantBuilder.kt b/thrifty-java-codegen/src/main/kotlin/com/microsoft.thrifty.gen/ConstantBuilder.kt index dd1f56c..950096a 100644 --- a/thrifty-java-codegen/src/main/kotlin/com/microsoft.thrifty.gen/ConstantBuilder.kt +++ b/thrifty-java-codegen/src/main/kotlin/com/microsoft.thrifty.gen/ConstantBuilder.kt @@ -65,9 +65,8 @@ internal class ConstantBuilder( initializer.addStatement("\$L = \$L", name, item) } - @Suppress("UNCHECKED_CAST") override fun visitList(listType: ListType) { - val list = value.value() as List + val list = value.getAsList() val elementType = listType.elementType().trueType val elementTypeName = typeResolver.getJavaClass(elementType) val genericName = ParameterizedTypeName.get(TypeNames.LIST, elementTypeName) @@ -75,9 +74,8 @@ internal class ConstantBuilder( generateSingleElementCollection(elementType, genericName, listImplName, list) } - @Suppress("UNCHECKED_CAST") override fun visitSet(setType: SetType) { - val set = value.value() as List + val set = value.getAsList() val elementType = setType.elementType().trueType val elementTypeName = typeResolver.getJavaClass(elementType) val genericName = ParameterizedTypeName.get(TypeNames.SET, elementTypeName) @@ -103,9 +101,8 @@ internal class ConstantBuilder( } } - @Suppress("UNCHECKED_CAST") override fun visitMap(mapType: MapType) { - val map = value.value() as Map + val map = value.getAsMap() val keyType = mapType.keyType().trueType val valueType = mapType.valueType().trueType @@ -167,22 +164,22 @@ internal class ConstantBuilder( private fun getNumberLiteral(element: ConstValueElement): Any { if (!element.isInt) { - throw AssertionError("Expected an int or double, got: " + element.kind()) + throw AssertionError("Expected an int or double, got: " + element.kind) } - return if (element.thriftText().startsWith("0x") || element.thriftText().startsWith("0X")) { - element.thriftText() + return if (element.thriftText.startsWith("0x") || element.thriftText.startsWith("0X")) { + element.thriftText } else { - element.asInt + element.getAsInt() } } override fun visitBool(boolType: BuiltinType): CodeBlock { val name: String - if (value.isIdentifier && ("true" == value.asString || "false" == value.asString)) { - name = if ("true" == value.value()) "true" else "false" + if (value.isIdentifier && ("true" == value.getAsString() || "false" == value.getAsString())) { + name = if ("true" == value.getAsString()) "true" else "false" } else if (value.isInt) { - name = if (value.value() as Long == 0L) "false" else "true" + name = if (value.getAsLong() == 0L) "false" else "true" } else { return constantOrError("Invalid boolean constant") } @@ -224,7 +221,7 @@ internal class ConstantBuilder( override fun visitDouble(doubleType: BuiltinType): CodeBlock { return if (value.isInt || value.isDouble) { - CodeBlock.builder().add("(double) \$L", value.asDouble).build() + CodeBlock.builder().add("(double) \$L", value.getAsDouble()).build() } else { constantOrError("Invalid double constant") } @@ -232,7 +229,7 @@ internal class ConstantBuilder( override fun visitString(stringType: BuiltinType): CodeBlock { return if (value.isString) { - CodeBlock.builder().add("\$S", value.asString).build() + CodeBlock.builder().add("\$S", value.getAsString()).build() } else { constantOrError("Invalid string constant") } @@ -249,19 +246,19 @@ internal class ConstantBuilder( override fun visitEnum(enumType: EnumType): CodeBlock { // TODO(ben): Figure out how to handle const references try { - val member = when (value.kind()) { + val member = when (value.kind) { ConstValueElement.Kind.INTEGER -> - enumType.findMemberById(value.asInt) + enumType.findMemberById(value.getAsInt()) ConstValueElement.Kind.IDENTIFIER -> { // Remove the enum name prefix, assuming it is present - val id = value.asString.split(".").last() + val id = value.getAsString().split(".").last() enumType.findMemberByName(id) } else -> throw AssertionError( - "Constant value kind " + value.kind() + " is not possibly an enum; validation bug") + "Constant value kind ${value.kind} is not possibly an enum; validation bug") } return CodeBlock.builder() @@ -270,13 +267,13 @@ internal class ConstantBuilder( } catch (e: NoSuchElementException) { throw IllegalStateException( - "No enum member in " + enumType.name() + " with value " + value.value()) + "No enum member in ${enumType.name()} with value ${value.value}") } } override fun visitList(listType: ListType): CodeBlock { if (value.isList) { - if (value.asList.isEmpty()) { + if (value.getAsList().isEmpty()) { val elementType = typeResolver.getJavaClass(listType.elementType()) return CodeBlock.builder() .add("\$T.<\$T>emptyList()", TypeNames.COLLECTIONS, elementType) @@ -290,7 +287,7 @@ internal class ConstantBuilder( override fun visitSet(setType: SetType): CodeBlock { if (value.isList) { // not a typo; ConstantValueElement.Kind.LIST covers lists and sets. - if (value.asList.isEmpty()) { + if (value.getAsList().isEmpty()) { val elementType = typeResolver.getJavaClass(setType.elementType()) return CodeBlock.builder() .add("\$T.<\$T>emptySet()", TypeNames.COLLECTIONS, elementType) @@ -304,7 +301,7 @@ internal class ConstantBuilder( override fun visitMap(mapType: MapType): CodeBlock { if (value.isMap) { - if (value.asMap.isEmpty()) { + if (value.getAsMap().isEmpty()) { val keyType = typeResolver.getJavaClass(mapType.keyType()) val valueType = typeResolver.getJavaClass(mapType.valueType()) return CodeBlock.builder() @@ -339,7 +336,7 @@ internal class ConstantBuilder( } private fun constantOrError(error: String): CodeBlock { - val message = "$error: ${value.value()} + at ${value.location()}" + val message = "$error: ${value.value} + at ${value.location}" if (!value.isIdentifier) { throw IllegalStateException(message) @@ -347,7 +344,7 @@ internal class ConstantBuilder( val expectedType = type.trueType - var name = value.asString + var name = value.getAsString() val ix = name.indexOf('.') var expectedProgram: String? = null if (ix != -1) { diff --git a/thrifty-java-codegen/src/main/kotlin/com/microsoft.thrifty.gen/ThriftyCodeGenerator.kt b/thrifty-java-codegen/src/main/kotlin/com/microsoft.thrifty.gen/ThriftyCodeGenerator.kt index feeb9bf..79041c1 100644 --- a/thrifty-java-codegen/src/main/kotlin/com/microsoft.thrifty.gen/ThriftyCodeGenerator.kt +++ b/thrifty-java-codegen/src/main/kotlin/com/microsoft.thrifty.gen/ThriftyCodeGenerator.kt @@ -888,7 +888,7 @@ class ThriftyCodeGenerator { } override fun visitList(listType: ListType) { - if (constant.value().asList.isEmpty()) { + if (constant.value().getAsList().isEmpty()) { field.initializer("\$T.emptyList()", TypeNames.COLLECTIONS) } else { initCollection("list", "unmodifiableList") @@ -896,7 +896,7 @@ class ThriftyCodeGenerator { } override fun visitSet(setType: SetType) { - if (constant.value().asList.isEmpty()) { + if (constant.value().getAsList().isEmpty()) { field.initializer("\$T.emptySet()", TypeNames.COLLECTIONS) } else { initCollection("set", "unmodifiableSet") @@ -904,7 +904,7 @@ class ThriftyCodeGenerator { } override fun visitMap(mapType: MapType) { - if (constant.value().asMap.isEmpty()) { + if (constant.value().getAsMap().isEmpty()) { field.initializer("\$T.emptyMap()", TypeNames.COLLECTIONS) } else { initCollection("map", "unmodifiableMap") diff --git a/thrifty-schema/Module.md b/thrifty-schema/Module.md new file mode 100644 index 0000000..f380be0 --- /dev/null +++ b/thrifty-schema/Module.md @@ -0,0 +1,9 @@ +# Module thrifty-schema + + + +# Package com.microsoft.thrifty.schema.parser + +Contains a Thrift parser implementation. + +The output of the parser is an untyped AST - types are not validated, typedefs are unresolved, etc. \ No newline at end of file diff --git a/thrifty-schema/build.gradle b/thrifty-schema/build.gradle index 6fc8a61..3811e06 100644 --- a/thrifty-schema/build.gradle +++ b/thrifty-schema/build.gradle @@ -19,7 +19,7 @@ * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. */ plugins { - id 'net.ltgt.apt' version '0.15' + id 'kotlin' id 'antlr' } @@ -39,15 +39,22 @@ dependencies { implementation libraries.okio + api libraries.kotlin + api 'com.google.code.findbugs:jsr305:3.0.1' api libraries.guava - api 'com.google.auto.value:auto-value-annotations:1.6' - annotationProcessor 'com.google.auto.value:auto-value:1.6' - testImplementation libraries.testing } compileTestJava { options.compilerArgs += [ '-Xep:ImmutableModification:OFF', '-Xlint:deprecation,unchecked' ] } + +// For some reason, Kotlin compilation is being run prior to antlr by default. +tasks['compileKotlin'].dependsOn('generateGrammarSource') +tasks['compileTestKotlin'].dependsOn('generateGrammarSource') +tasks['compileTestKotlin'].dependsOn('generateTestGrammarSource') +tasks['compileJava'].dependsOn('generateGrammarSource') +tasks['compileTestJava'].dependsOn('generateGrammarSource') +tasks['compileTestJava'].dependsOn('generateTestGrammarSource') diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/ErrorReporter.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/ErrorReporter.java index cca2bee..33ca479 100644 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/ErrorReporter.java +++ b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/ErrorReporter.java @@ -20,11 +20,11 @@ */ package com.microsoft.thrifty.schema; -import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class ErrorReporter { private boolean hasError = false; @@ -70,14 +70,46 @@ public class ErrorReporter { return builder.build(); } - @AutoValue - abstract static class Report { - public abstract Level level(); - public abstract Location location(); - public abstract String message(); + static class Report { + private final Level level; + private final Location location; + private final String message; + + public Level level() { + return level; + } + + public Location location() { + return location; + } + + public String message() { + return message; + } + + private Report(Level level, Location location, String message) { + this.level = level; + this.location = location; + this.message = message; + } static Report create(Level level, Location location, String message) { - return new AutoValue_ErrorReporter_Report(level, location, message); + return new Report(level, location, message); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Report report = (Report) o; + return level == report.level + && Objects.equals(location, report.location) + && Objects.equals(message, report.message); + } + + @Override + public int hashCode() { + return Objects.hash(level, location, message); } } diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/Linker.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/Linker.java index c5ece52..afb7f01 100644 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/Linker.java +++ b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/Linker.java @@ -387,7 +387,7 @@ class Linker { @Nonnull ThriftType resolveType(TypeElement type) { AnnotationElement annotationElement = type.annotations(); - ImmutableMap annotations = annotationElement != null + Map annotations = annotationElement != null ? annotationElement.values() : ImmutableMap.of(); diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/Loader.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/Loader.java index d5365a3..67940ef 100644 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/Loader.java +++ b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/Loader.java @@ -21,7 +21,6 @@ package com.microsoft.thrifty.schema; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; import com.google.common.io.Closeables; import com.microsoft.thrifty.schema.parser.IncludeElement; import com.microsoft.thrifty.schema.parser.ThriftFileElement; @@ -261,7 +260,7 @@ public final class Loader { loadedFiles.put(file.normalize().toAbsolutePath(), element); - ImmutableList includes = element.includes(); + List includes = element.includes(); if (includes.size() > 0) { includePaths.addFirst(dir); for (IncludeElement include : includes) { diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/AnnotationElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/AnnotationElement.java deleted file mode 100644 index c30196f..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/AnnotationElement.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableMap; -import com.microsoft.thrifty.schema.Location; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Map; - -@AutoValue -public abstract class AnnotationElement { - public abstract Location location(); - public abstract ImmutableMap values(); - - @Nullable - public String get(@Nonnull String name) { - return values().get(name); - } - - public boolean containsKey(@Nonnull String name) { - return values().containsKey(name); - } - - public boolean isEmpty() { - return values().isEmpty(); - } - - public int size() { - return values().size(); - } - - public static AnnotationElement create(Location location, Map values) { - return new AutoValue_AnnotationElement(location, ImmutableMap.copyOf(values)); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ConstElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ConstElement.java deleted file mode 100644 index 28bbb97..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ConstElement.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.microsoft.thrifty.schema.Location; - -import java.util.UUID; - -@AutoValue -public abstract class ConstElement { - public abstract Location location(); - public abstract String documentation(); - public abstract TypeElement type(); - public abstract String name(); - public abstract UUID uuid(); - public abstract ConstValueElement value(); - - public static Builder builder(Location location) { - return new AutoValue_ConstElement.Builder() - .location(location) - .documentation("") - .uuid(ThriftyParserPlugins.createUUID()); - } - - ConstElement() { } - - @AutoValue.Builder - public interface Builder { - Builder location(Location location); - Builder documentation(String documentation); - Builder type(TypeElement type); - Builder name(String name); - Builder uuid(UUID uuid); - Builder value(ConstValueElement value); - - ConstElement build(); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ConstValueElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ConstValueElement.java deleted file mode 100644 index 86ec0a8..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ConstValueElement.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.microsoft.thrifty.schema.Location; - -import java.util.List; -import java.util.Map; - -@AutoValue -public abstract class ConstValueElement { - public enum Kind { - INTEGER, - DOUBLE, - STRING, - IDENTIFIER, - LIST, - MAP, - } - - public abstract Location location(); - public abstract Kind kind(); - public abstract String thriftText(); - public abstract Object value(); - - public boolean isInt() { - return kind() == Kind.INTEGER; - } - - public boolean isDouble() { - return kind() == Kind.DOUBLE; - } - - public boolean isString() { - return kind() == Kind.STRING; - } - - public boolean isIdentifier() { - return kind() == Kind.IDENTIFIER; - } - - public boolean isList() { - return kind() == Kind.LIST; - } - - public boolean isMap() { - return kind() == Kind.MAP; - } - - public String getAsString() { - if (kind() == Kind.STRING || kind() == Kind.IDENTIFIER) { - return (String) value(); - } else { - throw new IllegalStateException("Cannot convert to string, kind=" + kind()); - } - } - - public long getAsLong() { - if (kind() == Kind.INTEGER) { - return (Long) value(); - } else { - throw new IllegalStateException("Cannot convert to long, kind=" + kind()); - } - } - - public int getAsInt() { - if (kind() == Kind.INTEGER) { - return ((Long) value()).intValue(); - } else { - throw new IllegalStateException("Cannot convert to long, kind=" + kind()); - } - } - - public double getAsDouble() { - if (kind() == Kind.DOUBLE) { - return (Double) value(); - } else { - throw new IllegalStateException("Cannot convert to double, kind=" + kind()); - } - } - - @SuppressWarnings("unchecked") - public List getAsList() { - if (kind() == Kind.LIST) { - return (List) value(); - } else { - throw new IllegalStateException("Cannot convert to list, kind=" + kind()); - } - } - - @SuppressWarnings("unchecked") - public Map getAsMap() { - if (kind() == Kind.MAP) { - return (Map) value(); - } else { - throw new IllegalStateException("Cannot convert to map, kind=" + kind()); - } - } - - ConstValueElement() { } - - public static ConstValueElement integer(Location location, String text, long value) { - return new AutoValue_ConstValueElement(location, Kind.INTEGER, text, value); - } - - public static ConstValueElement real(Location location, String text, double value) { - return new AutoValue_ConstValueElement(location, Kind.DOUBLE, text, value); - } - - public static ConstValueElement literal(Location location, String text, String value) { - return new AutoValue_ConstValueElement(location, Kind.STRING, text, value); - } - - public static ConstValueElement identifier(Location location, String text, String value) { - return new AutoValue_ConstValueElement(location, Kind.IDENTIFIER, text, value); - } - - public static ConstValueElement list(Location location, String text, List elements) { - return new AutoValue_ConstValueElement(location, Kind.LIST, text, ImmutableList.copyOf(elements)); - } - - public static ConstValueElement map( - Location location, - String text, - Map elements) { - return new AutoValue_ConstValueElement(location, Kind.MAP, text, ImmutableMap.copyOf(elements)); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/EnumElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/EnumElement.java deleted file mode 100644 index cfdefbb..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/EnumElement.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; -import com.microsoft.thrifty.schema.Location; - -import java.util.List; -import java.util.UUID; - -import javax.annotation.Nullable; - -@AutoValue -public abstract class EnumElement { - public abstract Location location(); - public abstract String documentation(); - public abstract String name(); - public abstract UUID uuid(); - public abstract ImmutableList members(); - @Nullable public abstract AnnotationElement annotations(); - - EnumElement() { } - - public static Builder builder(Location location) { - return new AutoValue_EnumElement.Builder() - .location(location) - .documentation("") - .uuid(ThriftyParserPlugins.createUUID()); - } - - @AutoValue.Builder - public interface Builder { - Builder location(Location location); - Builder documentation(String documentation); - Builder name(String name); - Builder uuid(UUID uuid); - Builder members(List members); - Builder annotations(AnnotationElement annotations); - - EnumElement build(); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/EnumMemberElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/EnumMemberElement.java deleted file mode 100644 index d929313..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/EnumMemberElement.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.microsoft.thrifty.schema.Location; - -import java.util.UUID; - -import javax.annotation.Nullable; - -@AutoValue -public abstract class EnumMemberElement { - public abstract Location location(); - public abstract String documentation(); - public abstract String name(); - public abstract UUID uuid(); - public abstract int value(); - @Nullable public abstract AnnotationElement annotations(); - - public static Builder builder(Location location) { - return new AutoValue_EnumMemberElement.Builder() - .location(location) - .documentation("") - .uuid(ThriftyParserPlugins.createUUID()); - } - - EnumMemberElement() { } - - @AutoValue.Builder - public interface Builder { - Builder location(Location location); - Builder documentation(String documentation); - Builder name(String name); - Builder uuid(UUID uuid); - Builder value(int value); - Builder annotations(AnnotationElement annotations); - - EnumMemberElement build(); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/FieldElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/FieldElement.java deleted file mode 100644 index 0024faf..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/FieldElement.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.microsoft.thrifty.schema.Location; -import com.microsoft.thrifty.schema.Requiredness; - -import java.util.UUID; - -import javax.annotation.Nullable; - -@AutoValue -public abstract class FieldElement { - public static Builder builder(Location location) { - return new AutoValue_FieldElement.Builder() - .location(location) - .documentation("") - .requiredness(Requiredness.DEFAULT) - .uuid(ThriftyParserPlugins.createUUID()); - } - - public abstract Location location(); - public abstract String documentation(); - public abstract int fieldId(); - public abstract Requiredness requiredness(); - public abstract TypeElement type(); - public abstract String name(); - public abstract UUID uuid(); - @Nullable public abstract ConstValueElement constValue(); - @Nullable public abstract AnnotationElement annotations(); - - FieldElement() { } - - @AutoValue.Builder - public interface Builder { - Builder location(Location location); - Builder documentation(String documentation); - Builder fieldId(int fieldId); - Builder requiredness(Requiredness requiredness); - Builder type(TypeElement type); - Builder name(String name); - Builder uuid(UUID uuid); - Builder constValue(ConstValueElement constValue); - Builder annotations(AnnotationElement annotations); - - FieldElement build(); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/FunctionElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/FunctionElement.java deleted file mode 100644 index 59c3a58..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/FunctionElement.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; -import com.microsoft.thrifty.schema.Location; - -import java.util.List; -import java.util.UUID; - -import javax.annotation.Nullable; - -@AutoValue -public abstract class FunctionElement { - public static Builder builder(Location location) { - return new AutoValue_FunctionElement.Builder() - .location(location) - .documentation("") - .oneWay(false) - .params(ImmutableList.of()) - .exceptions(ImmutableList.of()) - .uuid(ThriftyParserPlugins.createUUID()); - } - - public abstract Location location(); - public abstract String documentation(); - public abstract boolean oneWay(); - public abstract TypeElement returnType(); - public abstract String name(); - public abstract UUID uuid(); - public abstract ImmutableList params(); - public abstract ImmutableList exceptions(); - @Nullable public abstract AnnotationElement annotations(); - - FunctionElement() { } - - @AutoValue.Builder - public interface Builder { - Builder location(Location location); - Builder documentation(String documentation); - Builder oneWay(boolean oneWay); - Builder returnType(TypeElement returnType); - Builder name(String name); - Builder uuid(UUID uuid); - Builder params(List params); - Builder exceptions(List params); - Builder annotations(AnnotationElement annotations); - - FunctionElement build(); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/IncludeElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/IncludeElement.java deleted file mode 100644 index c0c7b14..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/IncludeElement.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.microsoft.thrifty.schema.Location; - -@AutoValue -public abstract class IncludeElement { - public abstract Location location(); - public abstract boolean isCpp(); - public abstract String path(); - - IncludeElement() { } - - public static IncludeElement create(Location location, boolean isCpp, String path) { - return new AutoValue_IncludeElement(location, isCpp, path); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ListTypeElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ListTypeElement.java deleted file mode 100644 index 6c6427a..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ListTypeElement.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.microsoft.thrifty.schema.Location; - -@AutoValue -public abstract class ListTypeElement extends TypeElement { - public abstract TypeElement elementType(); - - ListTypeElement() { - } - - public static ListTypeElement create( - Location location, - TypeElement element, - AnnotationElement annotations) { - String name = "list<" + element.name() + ">"; - return new AutoValue_ListTypeElement(location, name, annotations, element); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/MapTypeElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/MapTypeElement.java deleted file mode 100644 index b73d48f..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/MapTypeElement.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.microsoft.thrifty.schema.Location; - -@AutoValue -public abstract class MapTypeElement extends TypeElement { - public abstract TypeElement keyType(); - public abstract TypeElement valueType(); - - MapTypeElement() { - } - - public static MapTypeElement create( - Location location, - TypeElement key, - TypeElement value, - AnnotationElement annotations) { - String name = "map<" + key.name() + ", " + value.name() + ">"; - return new AutoValue_MapTypeElement(location, name, annotations, key, value); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/NamespaceElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/NamespaceElement.java deleted file mode 100644 index fcc157c..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/NamespaceElement.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.microsoft.thrifty.schema.Location; -import com.microsoft.thrifty.schema.NamespaceScope; - -import javax.annotation.Nullable; - -@AutoValue -public abstract class NamespaceElement { - public abstract Location location(); - public abstract NamespaceScope scope(); - public abstract String namespace(); - - @Nullable - public abstract AnnotationElement annotations(); - - NamespaceElement() { } - - public static Builder builder(Location location) { - return new AutoValue_NamespaceElement.Builder() - .location(location) - .annotations(null); - } - - @AutoValue.Builder - public interface Builder { - Builder location(Location location); - Builder scope(NamespaceScope scope); - Builder namespace(String namespace); - Builder annotations(AnnotationElement annotations); - - NamespaceElement build(); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ScalarTypeElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ScalarTypeElement.java deleted file mode 100644 index c8d230f..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ScalarTypeElement.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.microsoft.thrifty.schema.Location; - -@AutoValue -public abstract class ScalarTypeElement extends TypeElement { - ScalarTypeElement() { - } - - public static ScalarTypeElement create( - Location location, - String name, - AnnotationElement annotations) { - return new AutoValue_ScalarTypeElement(location, name, annotations); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ServiceElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ServiceElement.java deleted file mode 100644 index 4534389..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ServiceElement.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; -import com.microsoft.thrifty.schema.Location; - -import java.util.List; -import java.util.UUID; - -import javax.annotation.Nullable; - -@AutoValue -public abstract class ServiceElement { - public abstract Location location(); - public abstract String documentation(); - public abstract String name(); - public abstract UUID uuid(); - @Nullable public abstract TypeElement extendsService(); - public abstract ImmutableList functions(); - @Nullable public abstract AnnotationElement annotations(); - - ServiceElement() { } - - public static Builder builder(Location location) { - return new AutoValue_ServiceElement.Builder() - .location(location) - .documentation("") - .functions(ImmutableList.of()) - .uuid(ThriftyParserPlugins.createUUID()); - } - - @AutoValue.Builder - public interface Builder { - Builder location(Location location); - Builder documentation(String documentation); - Builder name(String name); - Builder uuid(UUID uuid); - Builder extendsService(TypeElement serviceName); - Builder functions(List functions); - Builder annotations(AnnotationElement annotations); - - ServiceElement build(); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/SetTypeElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/SetTypeElement.java deleted file mode 100644 index f57edee..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/SetTypeElement.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.microsoft.thrifty.schema.Location; - -@AutoValue -public abstract class SetTypeElement extends TypeElement { - public abstract TypeElement elementType(); - - SetTypeElement() { - } - - public static SetTypeElement create( - Location location, - TypeElement element, - AnnotationElement annotations) { - String name = "set<" + element.name() + ">"; - return new AutoValue_SetTypeElement(location, name, annotations, element); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/StructElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/StructElement.java deleted file mode 100644 index 586b18f..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/StructElement.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; -import com.microsoft.thrifty.schema.Location; - -import java.util.List; -import java.util.UUID; - -import javax.annotation.Nullable; - -/** - * Represents one of the three aggregate Thrift declarations: struct, union, or exception. - */ -@AutoValue -public abstract class StructElement { - public abstract Location location(); - public abstract String documentation(); - public abstract Type type(); - public abstract UUID uuid(); - public abstract String name(); - public abstract ImmutableList fields(); - @Nullable - public abstract AnnotationElement annotations(); - - public static Builder builder(Location location) { - return new AutoValue_StructElement.Builder() - .location(location) - .documentation("") - .uuid(ThriftyParserPlugins.createUUID()); - } - - StructElement() { } - - @AutoValue.Builder - public interface Builder { - Builder location(Location location); - Builder documentation(String documentation); - Builder type(Type type); - Builder uuid(UUID uuid); - Builder name(String name); - Builder fields(List fields); - Builder annotations(AnnotationElement annotations); - - StructElement build(); - } - - public enum Type { - STRUCT, - UNION, - EXCEPTION - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftFileElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftFileElement.java deleted file mode 100644 index c7e1ab5..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftFileElement.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; -import com.microsoft.thrifty.schema.Location; - -import java.util.List; - -@AutoValue -public abstract class ThriftFileElement { - public abstract Location location(); - public abstract ImmutableList namespaces(); - public abstract ImmutableList includes(); - public abstract ImmutableList constants(); - public abstract ImmutableList typedefs(); - public abstract ImmutableList enums(); - public abstract ImmutableList structs(); - public abstract ImmutableList unions(); - public abstract ImmutableList exceptions(); - public abstract ImmutableList services(); - - public static Builder builder(Location location) { - return new AutoValue_ThriftFileElement.Builder() - .location(location) - .namespaces(ImmutableList.of()) - .includes(ImmutableList.of()) - .constants(ImmutableList.of()) - .typedefs(ImmutableList.of()) - .enums(ImmutableList.of()) - .structs(ImmutableList.of()) - .unions(ImmutableList.of()) - .exceptions(ImmutableList.of()) - .services(ImmutableList.of()); - } - - ThriftFileElement() { } - - @AutoValue.Builder - public interface Builder { - Builder location(Location location); - Builder namespaces(List namespaces); - Builder includes(List includes); - Builder constants(List constants); - Builder typedefs(List typedefs); - Builder enums(List enums); - Builder structs(List structs); - Builder unions(List unions); - Builder exceptions(List exceptions); - Builder services(List services); - - ThriftFileElement build(); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftListener.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftListener.java deleted file mode 100644 index 5683b83..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftListener.java +++ /dev/null @@ -1,831 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.microsoft.thrifty.schema.ErrorReporter; -import com.microsoft.thrifty.schema.Location; -import com.microsoft.thrifty.schema.NamespaceScope; -import com.microsoft.thrifty.schema.Requiredness; -import com.microsoft.thrifty.schema.antlr.AntlrThriftBaseListener; -import com.microsoft.thrifty.schema.antlr.AntlrThriftLexer; -import com.microsoft.thrifty.schema.antlr.AntlrThriftParser; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.Lexer; -import org.antlr.v4.runtime.ParserRuleContext; -import org.antlr.v4.runtime.Token; -import org.antlr.v4.runtime.tree.ErrorNode; -import org.antlr.v4.runtime.tree.TerminalNode; - -import java.util.ArrayList; -import java.util.BitSet; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * A set of callbacks that, when used with a {@link org.antlr.v4.runtime.tree.ParseTreeWalker}, - * assemble a {@link ThriftFileElement} from an {@link AntlrThriftParser}. - * - * Instances of this class are single-use; after walking a parse tree, it will contain - * that parser's state as thrifty-schema parser elements. - */ -class ThriftListener extends AntlrThriftBaseListener { - // A number of tokens that should comfortably accommodate most input files - // without wildly re-allocating. Estimated based on the ClientTestThrift - // and TestThrift files, which contain around ~1200 tokens each. - private static final int INITIAL_BITSET_CAPACITY = 2048; - - private final CommonTokenStream tokenStream; - private final ErrorReporter errorReporter; - private final Location location; - - // We need to record which comment tokens have been treated as trailing documentation, - // so that scanning for leading doc tokens for subsequent elements knows where to stop. - // We can do this with a bitset tracking token indices of trailing-comment tokens. - private final BitSet trailingDocTokenIndexes = new BitSet(INITIAL_BITSET_CAPACITY); - - private final List includes = new ArrayList<>(); - private final List namespaces = new ArrayList<>(); - private final List enums = new ArrayList<>(); - private final List typedefs = new ArrayList<>(); - private final List structs = new ArrayList<>(); - private final List unions = new ArrayList<>(); - private final List exceptions = new ArrayList<>(); - private final List consts = new ArrayList<>(); - private final List services = new ArrayList<>(); - - /** - * Creates a new ThriftListener instance. - * - * @param tokenStream the same token stream used with the corresponding {@link AntlrThriftParser}; - * this stream will be queried for "hidden" tokens containing parsed doc - * comments. - * @param errorReporter an error reporting mechanism, used to communicate errors during parsing. - * @param location a location pointing at the beginning of the file being parsed. - */ - ThriftListener(CommonTokenStream tokenStream, ErrorReporter errorReporter, Location location) { - this.tokenStream = tokenStream; - this.errorReporter = errorReporter; - this.location = location; - } - - ThriftFileElement buildFileElement() { - return ThriftFileElement.builder(location) - .includes(includes) - .namespaces(namespaces) - .typedefs(typedefs) - .enums(enums) - .structs(structs) - .unions(unions) - .exceptions(exceptions) - .constants(consts) - .services(services) - .build(); - } - - @Override - public void exitInclude(AntlrThriftParser.IncludeContext ctx) { - TerminalNode pathNode = ctx.LITERAL(); - String path = unquote(locationOf(pathNode), pathNode.getText(), false); - - includes.add(IncludeElement.create(locationOf(ctx), false, path)); - } - - @Override - public void exitCppInclude(AntlrThriftParser.CppIncludeContext ctx) { - TerminalNode pathNode = ctx.LITERAL(); - String path = unquote(locationOf(pathNode), pathNode.getText(), false); - - includes.add(IncludeElement.create(locationOf(ctx), true, path)); - } - - @Override - public void exitStandardNamespace(AntlrThriftParser.StandardNamespaceContext ctx) { - String scopeName = ctx.namespaceScope().getText(); - String name = ctx.ns.getText(); - - AnnotationElement annotations = annotationsFromAntlr(ctx.annotationList()); - - NamespaceScope scope = NamespaceScope.forThriftName(scopeName); - if (scope == null) { - errorReporter.warn(locationOf(ctx.namespaceScope()), "Unknown namespace scope '" + scopeName + "'"); - return; - } - - NamespaceElement element = NamespaceElement.builder(locationOf(ctx)) - .scope(scope) - .namespace(name) - .annotations(annotations) - .build(); - - namespaces.add(element); - } - - @Override - public void exitPhpNamespace(AntlrThriftParser.PhpNamespaceContext ctx) { - NamespaceElement element = NamespaceElement.builder(locationOf(ctx)) - .scope(NamespaceScope.PHP) - .namespace(unquote(locationOf(ctx.LITERAL()), ctx.LITERAL().getText())) - .annotations(annotationsFromAntlr(ctx.annotationList())) - .build(); - - namespaces.add(element); - } - - @Override - public void exitXsdNamespace(AntlrThriftParser.XsdNamespaceContext ctx) { - errorReporter.error(locationOf(ctx), "'xsd_namespace' is unsupported"); - } - - @Override - public void exitSenum(AntlrThriftParser.SenumContext ctx) { - errorReporter.error(locationOf(ctx), "'senum' is unsupported; use 'enum' instead"); - } - - @Override - public void exitEnumDef(AntlrThriftParser.EnumDefContext ctx) { - String enumName = ctx.IDENTIFIER().getText(); - - int nextValue = 0; - Set values = new HashSet<>(); - - List members = new ArrayList<>(ctx.enumMember().size()); - for (AntlrThriftParser.EnumMemberContext memberContext : ctx.enumMember()) { - int value = nextValue; - - TerminalNode valueToken = memberContext.INTEGER(); - if (valueToken != null) { - value = parseInt(valueToken); - } - - if (!values.add(value)) { - errorReporter.error(locationOf(memberContext), "duplicate enum value: " + value); - continue; - } - - nextValue = value + 1; - - EnumMemberElement element = EnumMemberElement.builder(locationOf(memberContext)) - .name(memberContext.IDENTIFIER().getText()) - .value(value) - .documentation(formatJavadoc(memberContext)) - .annotations(annotationsFromAntlr(memberContext.annotationList())) - .build(); - - members.add(element); - } - - String doc = formatJavadoc(ctx); - EnumElement element = EnumElement.builder(locationOf(ctx)) - .name(enumName) - .documentation(doc) - .annotations(annotationsFromAntlr(ctx.annotationList())) - .members(members) - .build(); - - enums.add(element); - } - - @Override - public void exitStructDef(AntlrThriftParser.StructDefContext ctx) { - String name = ctx.IDENTIFIER().getText(); - ImmutableList fields = parseFieldList(ctx.field()); - - StructElement element = StructElement.builder(locationOf(ctx)) - .name(name) - .fields(fields) - .type(StructElement.Type.STRUCT) - .documentation(formatJavadoc(ctx)) - .annotations(annotationsFromAntlr(ctx.annotationList())) - .build(); - - structs.add(element); - } - - @Override - public void exitUnionDef(AntlrThriftParser.UnionDefContext ctx) { - String name = ctx.IDENTIFIER().getText(); - ImmutableList fields = parseFieldList(ctx.field()); - - int numFieldsWithDefaultValues = 0; - for (int i = 0; i < fields.size(); ++i) { - FieldElement element = fields.get(i); - if (element.requiredness() == Requiredness.REQUIRED) { - AntlrThriftParser.FieldContext fieldContext = ctx.field(i); - errorReporter.error(locationOf(fieldContext), "unions cannot have required fields"); - } - - if (element.constValue() != null) { - ++numFieldsWithDefaultValues; - } - } - - if (numFieldsWithDefaultValues > 1) { - errorReporter.error(locationOf(ctx), "unions can have at most one default value"); - } - - StructElement element = StructElement.builder(locationOf(ctx)) - .name(name) - .fields(fields) - .type(StructElement.Type.UNION) - .documentation(formatJavadoc(ctx)) - .annotations(annotationsFromAntlr(ctx.annotationList())) - .build(); - - unions.add(element); - } - - @Override - public void exitExceptionDef(AntlrThriftParser.ExceptionDefContext ctx) { - String name = ctx.IDENTIFIER().getText(); - ImmutableList fields = parseFieldList(ctx.field()); - - StructElement element = StructElement.builder(locationOf(ctx)) - .name(name) - .fields(fields) - .type(StructElement.Type.EXCEPTION) - .documentation(formatJavadoc(ctx)) - .annotations(annotationsFromAntlr(ctx.annotationList())) - .build(); - - exceptions.add(element); - } - - private ImmutableList parseFieldList(List contexts) { - return parseFieldList(contexts, Requiredness.DEFAULT); - } - - private ImmutableList parseFieldList( - List contexts, - Requiredness defaultRequiredness) { - ImmutableList.Builder builder = ImmutableList.builder(); - Set ids = new HashSet<>(); - - int nextValue = 1; - for (AntlrThriftParser.FieldContext fieldContext : contexts) { - FieldElement element = parseField(nextValue, fieldContext, defaultRequiredness); - if (element != null) { - builder = builder.add(element); - - if (!ids.add(element.fieldId())) { - errorReporter.error(locationOf(fieldContext), "duplicate field ID: " + element.fieldId()); - } - - if (element.fieldId() <= 0) { - errorReporter.error(locationOf(fieldContext), "field ID must be greater than zero"); - } - - if (element.fieldId() >= nextValue) { - nextValue = element.fieldId() + 1; - } - } else { - // assert-fail here? - ++nextValue; // this represents an error condition - } - } - - return builder.build(); - } - - private FieldElement parseField( - int defaultValue, - AntlrThriftParser.FieldContext ctx, - Requiredness defaultRequiredness) { - int fieldId = defaultValue; - if (ctx.INTEGER() != null) { - fieldId = parseInt(ctx.INTEGER()); - } - - String fieldName = ctx.IDENTIFIER().getText(); - - Requiredness requiredness = defaultRequiredness; - if (ctx.requiredness() != null) { - if (ctx.requiredness().getText().equals("required")) { - requiredness = Requiredness.REQUIRED; - } else if (ctx.requiredness().getText().equals("optional")) { - requiredness = Requiredness.OPTIONAL; - } else { - throw new AssertionError("Unexpected requiredness value: " + ctx.requiredness().getText()); - } - } - - return FieldElement.builder(locationOf(ctx)) - .documentation(formatJavadoc(ctx)) - .fieldId(fieldId) - .requiredness(requiredness) - .type(typeElementOf(ctx.fieldType())) - .name(fieldName) - .annotations(annotationsFromAntlr(ctx.annotationList())) - .constValue(constValueElementOf(ctx.constValue())) - .build(); - } - - @Override - public void exitTypedef(AntlrThriftParser.TypedefContext ctx) { - TypeElement oldType = typeElementOf(ctx.fieldType()); - - TypedefElement typedef = TypedefElement.builder(locationOf(ctx)) - .documentation(formatJavadoc(ctx)) - .annotations(annotationsFromAntlr(ctx.annotationList())) - .oldType(oldType) - .newName(ctx.IDENTIFIER().getText()) - .build(); - - typedefs.add(typedef); - } - - @Override - public void exitConstDef(AntlrThriftParser.ConstDefContext ctx) { - ConstElement element = ConstElement.builder(locationOf(ctx)) - .documentation(formatJavadoc(ctx)) - .type(typeElementOf(ctx.fieldType())) - .name(ctx.IDENTIFIER().getText()) - .value(constValueElementOf(ctx.constValue())) - .build(); - - consts.add(element); - } - - @Override - public void exitServiceDef(AntlrThriftParser.ServiceDefContext ctx) { - String name = ctx.name.getText(); - - ServiceElement.Builder builder = ServiceElement.builder(locationOf(ctx)) - .name(name) - .functions(parseFunctionList(ctx.function())) - .documentation(formatJavadoc(ctx)) - .annotations(annotationsFromAntlr(ctx.annotationList())); - - if (ctx.superType != null) { - TypeElement superType = typeElementOf(ctx.superType); - - if (!(superType instanceof ScalarTypeElement)) { - errorReporter.error(locationOf(ctx), "services cannot extend collections"); - } - - builder = builder.extendsService(superType); - } - - services.add(builder.build()); - } - - private ImmutableList parseFunctionList(List functionContexts) { - ImmutableList.Builder functions = ImmutableList.builder(); - - for (AntlrThriftParser.FunctionContext ctx : functionContexts) { - String name = ctx.IDENTIFIER().getText(); - - TypeElement returnType; - if (ctx.fieldType() != null) { - returnType = typeElementOf(ctx.fieldType()); - } else { - TerminalNode token = ctx.getToken(AntlrThriftLexer.VOID, 0); - if (token == null) { - throw new AssertionError("Function has no return type, and no VOID token - grammar error"); - } - Location loc = locationOf(token); - - // Do people actually annotation 'void'? We'll find out! - returnType = TypeElement.scalar(loc, "void", null); - } - - boolean isOneway = ctx.ONEWAY() != null; - - FunctionElement.Builder builder = FunctionElement.builder(locationOf(ctx)) - .oneWay(isOneway) - .returnType(returnType) - .name(name) - .documentation(formatJavadoc(ctx)) - .annotations(annotationsFromAntlr(ctx.annotationList())) - .params(parseFieldList(ctx.fieldList().field(), Requiredness.REQUIRED)); - - if (ctx.throwsList() != null) { - builder = builder.exceptions(parseFieldList(ctx.throwsList().fieldList().field())); - } - - functions.add(builder.build()); - } - - return functions.build(); - } - - @Override - public void visitErrorNode(ErrorNode node) { - errorReporter.error(locationOf(node), node.getText()); - } - - // region Utilities - - private AnnotationElement annotationsFromAntlr(AntlrThriftParser.AnnotationListContext ctx) { - if (ctx == null) { - return null; - } - - Map annotations = new LinkedHashMap<>(); - for (AntlrThriftParser.AnnotationContext annotationContext : ctx.annotation()) { - String name = annotationContext.IDENTIFIER().getText(); - String value; - if (annotationContext.LITERAL() != null) { - value = unquote(locationOf(annotationContext.LITERAL()), annotationContext.LITERAL().getText()); - } else { - value = "true"; - } - annotations.put(name, value); - } - - return AnnotationElement.create(locationOf(ctx), annotations); - } - - private Location locationOf(ParserRuleContext ctx) { - return locationOf(ctx.getStart()); - } - - private Location locationOf(TerminalNode node) { - return locationOf(node.getSymbol()); - } - - private Location locationOf(Token token) { - int line = token.getLine(); - int col = token.getCharPositionInLine() + 1; // Location.col is 1-based, Token.col is 0-based - return location.at(line, col); - } - - private String unquote(Location location, String literal) { - return unquote(location, literal, /* processEscapes */ true); - } - - private String unquote(Location location, String literal, boolean processEscapes) { - char[] chars = literal.toCharArray(); - char startChar = chars[0]; - char endChar = chars[chars.length - 1]; - - if (startChar != endChar || (startChar != '\'' && startChar != '"')) { - throw new AssertionError("Incorrect UNESCAPED_LITERAL rule: " + literal); - } - - StringBuilder sb = new StringBuilder(literal.length() - 2); - - int i = 1; - int end = chars.length - 1; - while (i < end) { - char c = chars[i++]; - - if (processEscapes && c == '\\') { - if (i == end) { - errorReporter.error(location, "Unterminated literal"); - break; - } - - char escape = chars[i++]; - switch (escape) { - case 'a': - sb.append((char) 0x7); - break; - case 'b': - sb.append('\b'); - break; - case 'f': - sb.append('\f'); - break; - case 'n': - sb.append('\n'); - break; - case 'r': - sb.append('\r'); - break; - case 't': - sb.append('\t'); - break; - case 'v': - sb.append((char) 0xB); - break; - case '\\': - sb.append('\\'); - break; - case 'u': - throw new UnsupportedOperationException("unicode escapes not yet implemented"); - default: - if (escape == startChar) { - sb.append(startChar); - } else { - errorReporter.error(location, "invalid escape character: " + escape); - } - } - } else { - sb.append(c); - } - } - - return sb.toString(); - } - - private TypeElement typeElementOf(AntlrThriftParser.FieldTypeContext context) { - if (context.baseType() != null) { - if (context.baseType().getText().equals("slist")) { - errorReporter.error(locationOf(context), "slist is unsupported; use list instead"); - } - - return TypeElement.scalar( - locationOf(context), - context.baseType().getText(), - annotationsFromAntlr(context.annotationList())); - } - - if (context.IDENTIFIER() != null) { - return TypeElement.scalar( - locationOf(context), - context.IDENTIFIER().getText(), - annotationsFromAntlr(context.annotationList())); - } - - if (context.containerType() != null) { - AntlrThriftParser.ContainerTypeContext containerContext = context.containerType(); - if (containerContext.mapType() != null) { - TypeElement keyType = typeElementOf(containerContext.mapType().key); - TypeElement valueType = typeElementOf(containerContext.mapType().value); - return TypeElement.map( - locationOf(containerContext.mapType()), - keyType, - valueType, - annotationsFromAntlr(context.annotationList())); - } - - if (containerContext.setType() != null) { - return TypeElement.set( - locationOf(containerContext.setType()), - typeElementOf(containerContext.setType().fieldType()), - annotationsFromAntlr(context.annotationList())); - } - - if (containerContext.listType() != null) { - return TypeElement.list( - locationOf(containerContext.listType()), - typeElementOf(containerContext.listType().fieldType()), - annotationsFromAntlr(context.annotationList())); - } - - throw new AssertionError("Unexpected container type - grammar error!"); - } - - throw new AssertionError("Unexpected type - grammar error!"); - } - - private ConstValueElement constValueElementOf(AntlrThriftParser.ConstValueContext ctx) { - if (ctx == null) { - return null; - } - - if (ctx.INTEGER() != null) { - String text = ctx.INTEGER().getText(); - - int radix = 10; - if (text.startsWith("0x") || text.startsWith("0X")) { - text = text.substring(2); - radix = 16; - } - - try { - long value = Long.parseLong(text, radix); - - return ConstValueElement.integer(locationOf(ctx), ctx.INTEGER().getText(), value); - } catch (NumberFormatException e) { - throw new AssertionError("Invalid integer accepted by ANTLR grammar: " + ctx.INTEGER().getText()); - } - } - - if (ctx.DOUBLE() != null) { - String text = ctx.DOUBLE().getText(); - - try { - double value = Double.parseDouble(text); - return ConstValueElement.real(locationOf(ctx), ctx.DOUBLE().getText(), value); - } catch (NumberFormatException e) { - throw new AssertionError("Invalid double accepted by ANTLR grammar: " + text); - } - } - - if (ctx.LITERAL() != null) { - String text = unquote(locationOf(ctx.LITERAL()), ctx.LITERAL().getText()); - return ConstValueElement.literal(locationOf(ctx), ctx.LITERAL().getText(), text); - } - - if (ctx.IDENTIFIER() != null) { - String id = ctx.IDENTIFIER().getText(); - return ConstValueElement.identifier(locationOf(ctx), ctx.IDENTIFIER().getText(), id); - } - - if (ctx.constList() != null) { - ImmutableList.Builder values = ImmutableList.builder(); - for (AntlrThriftParser.ConstValueContext valueContext : ctx.constList().constValue()) { - values.add(constValueElementOf(valueContext)); - } - return ConstValueElement.list(locationOf(ctx), ctx.constList().getText(), values.build()); - } - - if (ctx.constMap() != null) { - ImmutableMap.Builder values = ImmutableMap.builder(); - for (AntlrThriftParser.ConstMapEntryContext entry : ctx.constMap().constMapEntry()) { - ConstValueElement key = constValueElementOf(entry.key); - ConstValueElement value = constValueElementOf(entry.value); - values.put(key, value); - } - return ConstValueElement.map(locationOf(ctx), ctx.constMap().getText(), values.build()); - } - - throw new AssertionError("unreachable"); - } - - private static boolean isComment(Token token) { - switch (token.getType()) { - case AntlrThriftLexer.SLASH_SLASH_COMMENT: - case AntlrThriftLexer.HASH_COMMENT: - case AntlrThriftLexer.MULTILINE_COMMENT: - return true; - - default: - return false; - } - } - - private String formatJavadoc(ParserRuleContext context) { - List tokens = new ArrayList<>(); - tokens.addAll(getLeadingComments(context.getStart())); - tokens.addAll(getTrailingComments(context.getStop())); - - return formatJavadoc(tokens); - } - - private List getLeadingComments(Token token) { - List hiddenTokens = tokenStream.getHiddenTokensToLeft(token.getTokenIndex(), Lexer.HIDDEN); - - if (hiddenTokens == null || hiddenTokens.isEmpty()) { - return Collections.emptyList(); - } - - List comments = new ArrayList<>(hiddenTokens.size()); - for (Token hiddenToken : hiddenTokens) { - if (isComment(hiddenToken) && !trailingDocTokenIndexes.get(hiddenToken.getTokenIndex())) { - comments.add(hiddenToken); - } - } - - return comments; - } - - /** - * Read comments following the given token, until the first newline is encountered. - * - * INVARIANT: - * Assumes that the parse tree is being walked top-down, left to right! - * - * Trailing-doc tokens are marked as such, so that subsequent searches for "leading" - * doc don't grab tokens already used as "trailing" doc. If the walk order is *not* - * top-down, left-to-right, then the assumption underpinning the separation of leading - * and trailing comments is broken. - * - * @param endToken the token from which to search for trailing comment tokens. - * @return a list, possibly empty, of all trailing comment tokens. - */ - private List getTrailingComments(Token endToken) { - List hiddenTokens = tokenStream.getHiddenTokensToRight(endToken.getTokenIndex(), Lexer.HIDDEN); - - if (hiddenTokens == null || hiddenTokens.isEmpty()) { - return Collections.emptyList(); - } - - Token maybeTrailingDoc = hiddenTokens.get(0); // only one trailing comment is possible - - if (isComment(maybeTrailingDoc)) { - trailingDocTokenIndexes.set(maybeTrailingDoc.getTokenIndex()); - return Collections.singletonList(maybeTrailingDoc); - } - - return Collections.emptyList(); - } - - private static String formatJavadoc(List commentTokens) { - StringBuilder sb = new StringBuilder(); - - for (Token token : commentTokens) { - String text = token.getText(); - switch (token.getType()) { - case AntlrThriftLexer.SLASH_SLASH_COMMENT: - formatSingleLineComment(sb, text, "//"); - break; - - case AntlrThriftLexer.HASH_COMMENT: - formatSingleLineComment(sb, text, "#"); - break; - - case AntlrThriftLexer.MULTILINE_COMMENT: - formatMultilineComment(sb, text); - break; - - default: - // wut - break; - } - } - - String doc = sb.toString().trim(); - - if (!Strings.isNullOrEmpty(doc) && !doc.endsWith("\n")) { - doc += "\n"; - } - - return doc; - } - - private static void formatSingleLineComment(StringBuilder sb, String text, String prefix) { - int start = prefix.length(); - int end = text.length(); - - while (start < end && Character.isWhitespace(text.charAt(start))) { - ++start; - } - - while (end > start && Character.isWhitespace(text.charAt(end - 1))) { - --end; - } - - if (start != end) { - sb.append(text.substring(start, end)); - } - - sb.append("\n"); - } - - private static void formatMultilineComment(StringBuilder sb, String text) { - char[] chars = text.toCharArray(); - int pos = "/*".length(); - int length = chars.length; - boolean isStartOfLine = true; - - for (; pos + 1 < length; ++pos) { - char c = chars[pos]; - if (c == '*' && chars[pos + 1] == '/') { - sb.append("\n"); - return; - } - - if (c == '\n') { - sb.append(c); - isStartOfLine = true; - } else if (!isStartOfLine) { - sb.append(c); - } else if (c == '*') { - // skip a single subsequent space, if it exists - if (chars[pos + 1] == ' ') { - pos += 1; - } - - isStartOfLine = false; - } else if (! Character.isWhitespace(c)) { - sb.append(c); - isStartOfLine = false; - } - } - } - - private static int parseInt(TerminalNode node) { - return parseInt(node.getSymbol()); - } - - private static int parseInt(Token token) { - String text = token.getText(); - - int radix = 10; - if (text.startsWith("0x") || text.startsWith("0X")) { - radix = 16; - text = text.substring(2); - } - - return Integer.parseInt(text, radix); - } - - // endregion -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftParser.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftParser.java deleted file mode 100644 index c2be9ca..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftParser.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.common.base.Joiner; -import com.microsoft.thrifty.schema.ErrorReporter; -import com.microsoft.thrifty.schema.Location; -import com.microsoft.thrifty.schema.antlr.AntlrThriftLexer; -import com.microsoft.thrifty.schema.antlr.AntlrThriftParser; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CodePointCharStream; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.tree.ParseTreeWalker; - -import java.util.Locale; - -@SuppressWarnings({"WeakerAccess", "unused"}) // public methods are part of our "official" API surface -public final class ThriftParser { - - /** - * Parse the given Thrift {@code text}, using the given {@code location} - * to anchor parsed elements withing the file. - * @param location the {@link Location} of the data being parsed. - * @param text the text to be parsed. - * @return a representation of the parsed Thrift data. - */ - public static ThriftFileElement parse(Location location, String text) { - return parse(location, text, new ErrorReporter()); - } - - /** - * Parse the given Thrift {@code text}, using the given {@code location} - * to anchor parsed elements withing the file. - * @param location the {@link Location} of the data being parsed. - * @param text the text to be parsed. - * @param reporter an {@link ErrorReporter} to collect warnings. - * @return a representation of the parsed Thrift data. - */ - public static ThriftFileElement parse(Location location, String text, ErrorReporter reporter) { - CodePointCharStream charStream = CharStreams.fromString(text, location.path()); - AntlrThriftLexer lexer = new AntlrThriftLexer(charStream); - CommonTokenStream tokenStream = new CommonTokenStream(lexer); - AntlrThriftParser antlrParser = new AntlrThriftParser(tokenStream); - - ThriftListener thriftListener = new ThriftListener(tokenStream, reporter, location); - - ParseTreeWalker walker = new ParseTreeWalker(); - walker.walk(thriftListener, antlrParser.document()); - - if (reporter.hasError()) { - String errorReports = Joiner.on('\n').join(reporter.formattedReports()); - String message = String.format(Locale.US, "Syntax errors in %s:\n%s", location, errorReports); - throw new IllegalStateException(message); - } - - return thriftListener.buildFileElement(); - } - - private ThriftParser() { - // no instances - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftyParserPlugins.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftyParserPlugins.java deleted file mode 100644 index 584ce6e..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/ThriftyParserPlugins.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.microsoft.thrifty.schema.parser; - -import java.util.UUID; - -/** - * Utility class to inject handlers to certain standard Thrifty operations. - */ -public final class ThriftyParserPlugins { - - private static final UUIDProvider DEFAULT_UUID_PROVIDER = UUID::randomUUID; - private static volatile UUIDProvider uuidProvider = DEFAULT_UUID_PROVIDER; - - /** - * Prevents changing the plugins. - */ - private static volatile boolean lockdown; - - /** - * Prevents changing the plugins from then on. - *

- * This allows container-like environments to prevent client messing with plugins. - */ - public static void lockdown() { - lockdown = true; - } - - /** - * Returns true if the plugins were locked down. - * - * @return true if the plugins were locked down - */ - public static boolean isLockdown() { - return lockdown; - } - - /** - * @param uuidProvider the provider to use for generating {@link UUID}s for elements. - */ - public static void setUUIDProvider(UUIDProvider uuidProvider) { - if (lockdown) { - throw new IllegalStateException("Plugins can't be changed anymore"); - } - ThriftyParserPlugins.uuidProvider = uuidProvider; - } - - /** - * @return a {@link UUID} as dictated by {@link #uuidProvider}. Default is random UUIDs. - */ - public static UUID createUUID() { - return uuidProvider.call(); - } - - public static void reset() { - uuidProvider = DEFAULT_UUID_PROVIDER; - } - - private ThriftyParserPlugins() { - // No instances. - } - - /** - * A simple provider interface for creating {@link UUID}s. - */ - public interface UUIDProvider { - - /** - * @return a {@link UUID}. - */ - UUID call(); - } - -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/TypeElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/TypeElement.java deleted file mode 100644 index 20cde3a..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/TypeElement.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.microsoft.thrifty.schema.Location; - -import javax.annotation.Nullable; - -public abstract class TypeElement { - public abstract Location location(); - public abstract String name(); - - @Nullable - public abstract AnnotationElement annotations(); - - public static TypeElement scalar(Location location, String name, AnnotationElement annotations) { - return ScalarTypeElement.create(location, name, annotations); - } - - public static TypeElement list( - Location location, - TypeElement elementType, - AnnotationElement annotations) { - return ListTypeElement.create(location, elementType, annotations); - } - - public static TypeElement set( - Location location, - TypeElement elementType, - AnnotationElement annotations) { - return SetTypeElement.create(location, elementType, annotations); - } - - public static TypeElement map( - Location location, - TypeElement key, - TypeElement value, - AnnotationElement annotations) { - return MapTypeElement.create(location, key, value, annotations); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/TypedefElement.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/TypedefElement.java deleted file mode 100644 index a8af60d..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/TypedefElement.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.auto.value.AutoValue; -import com.microsoft.thrifty.schema.Location; - -import java.util.UUID; - -import javax.annotation.Nullable; - -@AutoValue -public abstract class TypedefElement { - public abstract Location location(); - public abstract String documentation(); - public abstract TypeElement oldType(); - public abstract String newName(); - public abstract UUID uuid(); - - @Nullable - public abstract AnnotationElement annotations(); - - TypedefElement() { } - - public static Builder builder(Location location) { - return new AutoValue_TypedefElement.Builder() - .location(location) - .documentation("") - .uuid(ThriftyParserPlugins.createUUID()); - } - - @AutoValue.Builder - public interface Builder { - Builder location(Location location); - Builder documentation(String documentation); - Builder oldType(TypeElement oldType); - Builder newName(String newName); - Builder uuid(UUID uuid); - Builder annotations(AnnotationElement annotations); - - TypedefElement build(); - } -} diff --git a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/package-info.java b/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/package-info.java deleted file mode 100644 index d86e1dd..0000000 --- a/thrifty-schema/src/main/java/com/microsoft/thrifty/schema/parser/package-info.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ - -/** - * Contains a Thrift parser implementation. - * - * The output of the parser is an untyped AST - types are not - * validated, typedefs are unresolved, etc. - */ -package com.microsoft.thrifty.schema.parser; diff --git a/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ConstValueElement.kt b/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ConstValueElement.kt new file mode 100644 index 0000000..6612727 --- /dev/null +++ b/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ConstValueElement.kt @@ -0,0 +1,185 @@ +/* + * Thrifty + * + * Copyright (c) Microsoft Corporation + * + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING + * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, + * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. + * + * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. + */ +package com.microsoft.thrifty.schema.parser + +import com.microsoft.thrifty.schema.Location + +/** + * Represents a literal value in a Thrift file for a constant or a field's + * default value. + */ +// TODO: Convert this to a sealed class +data class ConstValueElement( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The kind of constant value this is - integer, real, list, identifier, etc. + */ + @get:JvmName("kind") + val kind: Kind, + + /** + * The actual Thrift text comprising this const value. + */ + @get:JvmName("thriftText") + val thriftText: String, + + /** + * The parsed value itself. Will be of a type dictated by the value's [kind]. + */ + @get:JvmName("value") + val value: Any +) { + @get:JvmName("isInt") + val isInt: Boolean + get() = kind == Kind.INTEGER + + @get:JvmName("isDouble") + val isDouble: Boolean + get() = kind == Kind.DOUBLE + + @get:JvmName("isString") + val isString: Boolean + get() = kind == Kind.STRING + + @get:JvmName("isIdentifier") + val isIdentifier: Boolean + get() = kind == Kind.IDENTIFIER + + @get:JvmName("isList") + val isList: Boolean + get() = kind == Kind.LIST + + @get:JvmName("isMap") + val isMap: Boolean + get() = kind == Kind.MAP + + fun getAsInt(): Int { + check(isInt) { "Cannot convert to int; kind=$kind" } + return (value as Long).toInt() + } + + fun getAsLong(): Long { + check(isInt) { "Cannot convert to long; kind=$kind" } + return value as Long + } + + fun getAsDouble(): Double { + check(isDouble) { "Cannot convert to double; kind=$kind" } + return value as Double + } + + fun getAsString(): String { + check(isString || isIdentifier) { "Cannot convert to string; kind=$kind" } + return value as String + } + + @Suppress("UNCHECKED_CAST") + fun getAsList(): List { + check(isList) { "Cannot convert to list; kind=$kind"} + return value as List + } + + @Suppress("UNCHECKED_CAST") + fun getAsMap(): Map { + check(isMap) { "Cannot convert to map; kind=$kind" } + return value as Map + } + + /** + * Defines the kinds of values representable as a [ConstValueElement]. + */ + enum class Kind { + /** + * Ye Olde Integer. + * + * Will actually be a [Long], at runtime. + */ + INTEGER, + + /** + * A 64-it floating-point number. + */ + DOUBLE, + + /** + * A quoted string. + */ + STRING, + + /** + * An unquoted string, naming some other Thrift entity. + */ + IDENTIFIER, + + /** + * A list of [ConstValueElements][ConstValueElement] + * + * It is assumed that all values in the list share the same type; this + * is not enforced by the parser. + */ + LIST, + + /** + * An key-value mapping of [ConstValueElements][ConstValueElement]. + * + * It is assumed that all keys share the same type, and that all values + * also share the same type. This is not enforced by the parser. + */ + MAP, + } + + companion object { + @JvmStatic + fun integer(location: Location, text: String, value: Long): ConstValueElement { + return ConstValueElement(location, Kind.INTEGER, text, value) + } + + @JvmStatic + fun real(location: Location, text: String, value: Double): ConstValueElement { + return ConstValueElement(location, Kind.DOUBLE, text, value) + } + + @JvmStatic + fun literal(location: Location, text: String, value: String): ConstValueElement { + return ConstValueElement(location, Kind.STRING, text, value) + } + + @JvmStatic + fun identifier(location: Location, text: String, value: String): ConstValueElement { + return ConstValueElement(location, Kind.IDENTIFIER, text, value) + } + + @JvmStatic + fun list(location: Location, text: String, value: List): ConstValueElement { + return ConstValueElement(location, Kind.LIST, text, value.toList()) + } + + @JvmStatic + fun map(location: Location, text: String, value: Map): ConstValueElement { + return ConstValueElement(location, Kind.MAP, text, value.toMap()) + } + } +} \ No newline at end of file diff --git a/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ParserTypes.kt b/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ParserTypes.kt new file mode 100644 index 0000000..20a445e --- /dev/null +++ b/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ParserTypes.kt @@ -0,0 +1,677 @@ +/* + * Thrifty + * + * Copyright (c) Microsoft Corporation + * + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING + * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, + * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. + * + * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. + */ +package com.microsoft.thrifty.schema.parser + +import com.microsoft.thrifty.schema.Location +import com.microsoft.thrifty.schema.NamespaceScope +import com.microsoft.thrifty.schema.Requiredness +import java.util.UUID + +/** + * Represents an instance of one or more Thrift annotations. + * + * @constructor Creates a new instance of [AnnotationElement]. + */ +data class AnnotationElement( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The annotation values. + */ + @get:JvmName("values") + val values: Map +) { + /** + * True if this element contains no annotation values, otherwise false. + */ + @get:JvmName("isEmpty") + val isEmpty: Boolean + get() = values.isEmpty() + + /** + * The number of annotation values in this element. + */ + @get:JvmName("size") + val size: Int + get() = values.size + + /** + * Gets the value of the given annotation [key], if present. + */ + operator fun get(key: String): String? = values[key] +} + +/** + * Represents the inclusion of one Thrift program into another. + */ +data class IncludeElement( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * Indicates whether or not this is a `cpp_include` statement. + */ + @get:JvmName("isCpp") + val isCpp: Boolean, + + /** + * The path (relative or absolute) of the included program. + */ + @get:JvmName("path") + val path: String +) + +/** + * Represents the declaration of a new name for an existing type. + */ +data class TypedefElement @JvmOverloads constructor( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The type receiving a new name. + */ + @get:JvmName("oldType") + val oldType: TypeElement, + + /** + * The new name for [oldType]. + */ + @get:JvmName("newName") + val newName: String, + + /** + * The documentation associated with this element, if any. + */ + @get:JvmName("documentation") + val documentation: String = "", + + /** + * A UUID uniquely identifying this element. + */ + @get:JvmName("uuid") + val uuid: UUID = ThriftyParserPlugins.createUUID(), + + /** + * The annotations associated with this element, if any. + */ + @get:JvmName("annotations") + val annotations: AnnotationElement? = null +) + +/** + * Represents the declaration of a language-specific namespace in a Thrift + * program. + */ +data class NamespaceElement @JvmOverloads constructor( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The language ("scope") to which this declaration applies. + */ + @get:JvmName("scope") + val scope: NamespaceScope, + + /** + * The name of the namespace. + */ + @get:JvmName("namespace") + val namespace: String, + + /** + * The annotations associated with this element, if any. + */ + @get:JvmName("annotations") + val annotations: AnnotationElement? = null +) + +/** + * Represents the declaration of a named constant value in a Thrift program. + */ +data class ConstElement @JvmOverloads constructor( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The type of the constant's value. + */ + @get:JvmName("type") + val type: TypeElement, + + /** + * The name of the constant. + */ + @get:JvmName("name") + val name: String, + + /** + * The literal value of the constant. + * + * This is not guaranteed by the parser to conform to the declared [type]. + */ + @get:JvmName("value") + val value: ConstValueElement, + + /** + * The documentation associated with this element, if any. + */ + @get:JvmName("documentation") + val documentation: String = "", + + /** + * A UUID uniquely identifying this element. + */ + @get:JvmName("uuid") + val uuid: UUID = ThriftyParserPlugins.createUUID() +) + +/** + * Represents a single named member of a Thrift enumeration. + */ +data class EnumMemberElement @JvmOverloads constructor( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The name of the enum member. + */ + @get:JvmName("name") + val name: String, + + /** + * The integral value associated with the enum member. + */ + @get:JvmName("value") + val value: Int, + + /** + * The documentation associated with this element, if any. + */ + @get:JvmName("documentation") + val documentation: String = "", + + /** + * The annotations associated with the enum member, if any. + */ + @get:JvmName("annotations") + val annotations: AnnotationElement? = null, + + /** + * A UUID uniquely identifying this element. + */ + @get:JvmName("uuid") + val uuid: UUID = ThriftyParserPlugins.createUUID() +) + +/** + * Represents a Thrift enumeration. + */ +data class EnumElement @JvmOverloads constructor( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The name of the enumeration. + */ + @get:JvmName("name") + val name: String, + + /** + * The members comprising this enumeration. + */ + @get:JvmName("members") + val members: List, + + /** + * The documentation associated with this enumeration, if any. + */ + @get:JvmName("documentation") + val documentation: String = "", + + /** + * The annotations associated with this element, if any. + */ + @get:JvmName("annotations") + val annotations: AnnotationElement? = null, + + /** + * A UUID uniquely identifying this element. + */ + @get:JvmName("uuid") + val uuid: UUID = ThriftyParserPlugins.createUUID() +) + +/** + * Represents a field in a Thrift struct, union, or exception. + */ +data class FieldElement @JvmOverloads constructor( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The integer ID of the field. + */ + @get:JvmName("fieldId") + val fieldId: Int, + + /** + * The type of the field. + */ + @get:JvmName("type") + val type: TypeElement, + + /** + * The name of the field. + */ + @get:JvmName("name") + val name: String, + + /** + * The [Requiredness] of the field. + */ + @get:JvmName("requiredness") + val requiredness: Requiredness = Requiredness.DEFAULT, + + /** + * The documentation associated with the field, if any. + */ + @get:JvmName("documentation") + val documentation: String = "", + + /** + * The default value of the field, if any. + */ + @get:JvmName("constValue") + val constValue: ConstValueElement? = null, + + /** + * The annotations associated with the field, if any. + */ + @get:JvmName("annotations") + val annotations: AnnotationElement? = null, + + /** + * A UUID uniquely identifying this element. + */ + @get:JvmName("uuid") + val uuid: UUID = ThriftyParserPlugins.createUUID() +) + +/** + * Represents the definition of a Thrift struct, union, or exception. + */ +data class StructElement @JvmOverloads constructor( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The name of the struct, union, or exception. + */ + @get:JvmName("name") + val name: String, + + /** + * The kind of struct represented by this element: struct, union, or exception. + */ + @get:JvmName("type") + val type: Type, + + /** + * The fields comprising this struct. + */ + @get:JvmName("fields") + val fields: List, + + /** + * The documentation associated with this struct, if any. + */ + @get:JvmName("documentation") + val documentation: String = "", + + /** + * The annotations associated with this element, if any. + */ + @get:JvmName("annotations") + val annotations: AnnotationElement? = null, + + /** + * A UUID uniquely identifying this element. + */ + @get:JvmName("uuid") + val uuid: UUID = ThriftyParserPlugins.createUUID() +) { + /** + * Defines the different types of structured element in the Thrift language. + */ + enum class Type { + /** + * A struct, in the C sense of the term. That is, an ordered set of + * named fields having heterogeneous types. + */ + STRUCT, + + /** + * A union, also in the C sense of the term. A set of named fields, of + * which at most one may have a value at the same time. + */ + UNION, + + /** + * An exception is like a [STRUCT], except that it serves as an error + * type that communicates failure from an RPC call. Declared as part + * of a [FunctionElement]. + */ + EXCEPTION + } +} + +/** + * Represents an RPC function declaration. + * + * Functions are always declared within a [ServiceElement]. + */ +data class FunctionElement @JvmOverloads constructor( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The name of the function. + */ + @get:JvmName("name") + val name: String, + + /** + * The return type of the function. May be `void` to indicate no + * return type. + */ + @get:JvmName("returnType") + val returnType: TypeElement, + + /** + * A list, possibly empty, of function parameters. + */ + @get:JvmName("params") + val params: List = emptyList(), + + /** + * A list, possibly empty, of exceptions thrown by this function. + */ + @get:JvmName("exceptions") + val exceptions: List = emptyList(), + + /** + * True if the function is `oneway`, otherwise false. + * + * A function declared with the `oneway` keyword has no return type, + * and generated service clients will not wait for a response from + * the remote endpoint when making a one-way RPC call. + * + * One-way functions must have a return type of `void`; this is not + * validated at parse time. + */ + @get:JvmName("oneWay") + val oneWay: Boolean = false, + + /** + * The documentation associated with this function, if any. + */ + @get:JvmName("documentation") + val documentation: String = "", + + /** + * The annotations associated with this function, if any. + */ + @get:JvmName("annotations") + val annotations: AnnotationElement? = null, + + /** + * A UUID uniquely identifying this element. + */ + @get:JvmName("uuid") + val uuid: UUID = ThriftyParserPlugins.createUUID() +) + +/** + * Represents the declaration of a Thrift service. + * + * A service is an entity having zero or more functions that can be invoked by + * remote clients. Services can inherit the definition of other services, very + * much like inheritance in an object-oriented language. + */ +data class ServiceElement @JvmOverloads constructor( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The name of the service. + */ + @get:JvmName("name") + val name: String, + + /** + * The list, possibly empty, of functions defined by this service. + */ + @get:JvmName("functions") + val functions: List = emptyList(), + + /** + * The base type, if any, of this service. + * + * A base type is presumed to refer to another service, but that is not + * enforced by the parser. + */ + @get:JvmName("extendsService") + val extendsService: TypeElement? = null, + + /** + * The documentation associated with this service, if any. + */ + @get:JvmName("documentation") + val documentation: String = "", + + /** + * The annotations associated with this service, if any. + */ + @get:JvmName("annotations") + val annotations: AnnotationElement? = null, + + /** + * A UUID uniquely identifying this element. + */ + @get:JvmName("uuid") + val uuid: UUID = ThriftyParserPlugins.createUUID() +) + +/** + * Represents a Thrift file, and everything defined within it. + */ +data class ThriftFileElement( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The list of all namespaces defined within this file. + */ + @get:JvmName("namespaces") + val namespaces: List = emptyList(), + + /** + * The list of all other thrift files included by this file. + */ + @get:JvmName("includes") + val includes: List = emptyList(), + + /** + * The list of all constants defined within this file. + */ + @get:JvmName("constants") + val constants: List = emptyList(), + + /** + * The list of all typedefs defined within this file. + */ + @get:JvmName("typedefs") + val typedefs: List = emptyList(), + + /** + * The list of all enums defined within this file. + */ + @get:JvmName("enums") + val enums: List = emptyList(), + + /** + * The list of all structs defined within this file. + */ + @get:JvmName("structs") + val structs: List = emptyList(), + + /** + * The list of all unions defined within this file. + */ + @get:JvmName("unions") + val unions: List = emptyList(), + + /** + * The list of all exceptions defined within this file. + */ + @get:JvmName("exceptions") + val exceptions: List = emptyList(), + + /** + * The list of all services defined within this file. + */ + @get:JvmName("services") + val services: List = emptyList() +) { + class Builder(private val location: Location) { + private var namespaces = emptyList() + private var includes = emptyList() + private var constants = emptyList() + private var typedefs = emptyList() + private var enums = emptyList() + private var structs = emptyList() + private var unions = emptyList() + private var exceptions = emptyList() + private var services: List = emptyList() + + fun namespaces(namespaces: List): Builder { + this.namespaces = namespaces + return this + } + + fun includes(includes: List): Builder { + this.includes = includes + return this + } + + fun constants(constants: List): Builder { + this.constants = constants + return this + } + + fun typedefs(typedefs: List): Builder { + this.typedefs = typedefs + return this + } + + fun enums(enums: List): Builder { + this.enums = enums + return this + } + + fun structs(structs: List): Builder { + this.structs = structs + return this + } + + fun unions(unions: List): Builder { + this.unions = unions + return this + } + + fun exceptions(exceptions: List): Builder { + this.exceptions = exceptions + return this + } + + fun services(services: List): Builder { + this.services = services + return this + } + + fun build(): ThriftFileElement { + return ThriftFileElement( + location = location, + namespaces = namespaces, + includes = includes, + constants = constants, + typedefs = typedefs, + enums = enums, + structs = structs, + unions = unions, + exceptions = exceptions, + services = services + ) + } + } + + companion object { + @JvmStatic fun builder(location: Location) = Builder(location) + } +} \ No newline at end of file diff --git a/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ThriftListener.kt b/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ThriftListener.kt new file mode 100644 index 0000000..dca9300 --- /dev/null +++ b/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ThriftListener.kt @@ -0,0 +1,781 @@ +/* + * Thrifty + * + * Copyright (c) Microsoft Corporation + * + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING + * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, + * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. + * + * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. + */ +package com.microsoft.thrifty.schema.parser + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.microsoft.thrifty.schema.ErrorReporter +import com.microsoft.thrifty.schema.Location +import com.microsoft.thrifty.schema.NamespaceScope +import com.microsoft.thrifty.schema.Requiredness +import com.microsoft.thrifty.schema.antlr.AntlrThriftBaseListener +import com.microsoft.thrifty.schema.antlr.AntlrThriftLexer +import com.microsoft.thrifty.schema.antlr.AntlrThriftParser +import org.antlr.v4.runtime.CommonTokenStream +import org.antlr.v4.runtime.Lexer +import org.antlr.v4.runtime.ParserRuleContext +import org.antlr.v4.runtime.Token +import org.antlr.v4.runtime.tree.ErrorNode +import org.antlr.v4.runtime.tree.TerminalNode +import java.util.BitSet + +/** + * A set of callbacks that, when used with a [org.antlr.v4.runtime.tree.ParseTreeWalker], + * assemble a [ThriftFileElement] from an [AntlrThriftParser]. + * + * Instances of this class are single-use; after walking a parse tree, it will contain + * that parser's state as thrifty-schema parser elements. + * + * @param tokenStream the same token stream used with the corresponding [AntlrThriftParser]; + * this stream will be queried for "hidden" tokens containing parsed doc + * comments. + * @param errorReporter an error reporting mechanism, used to communicate errors during parsing. + * @param location a location pointing at the beginning of the file being parsed. + */ +internal class ThriftListener( + private val tokenStream: CommonTokenStream, + private val errorReporter: ErrorReporter, + private val location: Location +) : AntlrThriftBaseListener() { + + // We need to record which comment tokens have been treated as trailing documentation, + // so that scanning for leading doc tokens for subsequent elements knows where to stop. + // We can do this with a bitset tracking token indices of trailing-comment tokens. + private val trailingDocTokenIndexes = BitSet(INITIAL_BITSET_CAPACITY) + + private val includes = mutableListOf() + private val namespaces = mutableListOf() + private val enums = mutableListOf() + private val typedefs = mutableListOf() + private val structs = mutableListOf() + private val unions = mutableListOf() + private val exceptions = mutableListOf() + private val consts = mutableListOf() + private val services = mutableListOf() + + fun buildFileElement(): ThriftFileElement { + return ThriftFileElement.builder(location) + .includes(includes) + .namespaces(namespaces) + .typedefs(typedefs) + .enums(enums) + .structs(structs) + .unions(unions) + .exceptions(exceptions) + .constants(consts) + .services(services) + .build() + } + + override fun exitInclude(ctx: AntlrThriftParser.IncludeContext) { + val pathNode = ctx.LITERAL() + val path = unquote(locationOf(pathNode), pathNode.text, false) + + includes.add(IncludeElement(locationOf(ctx), false, path)) + } + + override fun exitCppInclude(ctx: AntlrThriftParser.CppIncludeContext) { + val pathNode = ctx.LITERAL() + val path = unquote(locationOf(pathNode), pathNode.text, false) + + includes.add(IncludeElement(locationOf(ctx), true, path)) + } + + override fun exitStandardNamespace(ctx: AntlrThriftParser.StandardNamespaceContext) { + val scopeName = ctx.namespaceScope().text + val name = ctx.ns.text + + val annotations = annotationsFromAntlr(ctx.annotationList()) + + val scope = NamespaceScope.forThriftName(scopeName) + if (scope == null) { + errorReporter.warn(locationOf(ctx.namespaceScope()), "Unknown namespace scope '$scopeName'") + return + } + + val element = NamespaceElement( + location = locationOf(ctx), + scope = scope, + namespace = name, + annotations = annotations) + + namespaces.add(element) + } + + override fun exitPhpNamespace(ctx: AntlrThriftParser.PhpNamespaceContext) { + val element = NamespaceElement( + locationOf(ctx), + scope = NamespaceScope.PHP, + namespace = unquote(locationOf(ctx.LITERAL()), ctx.LITERAL().text), + annotations = annotationsFromAntlr(ctx.annotationList())) + + namespaces.add(element) + } + + override fun exitXsdNamespace(ctx: AntlrThriftParser.XsdNamespaceContext) { + errorReporter.error(locationOf(ctx), "'xsd_namespace' is unsupported") + } + + override fun exitSenum(ctx: AntlrThriftParser.SenumContext) { + errorReporter.error(locationOf(ctx), "'senum' is unsupported; use 'enum' instead") + } + + override fun exitEnumDef(ctx: AntlrThriftParser.EnumDefContext) { + val enumName = ctx.IDENTIFIER().text + + var nextValue = 0 + val values = mutableSetOf() + + val members = ArrayList(ctx.enumMember().size) + for (memberContext in ctx.enumMember()) { + var value = nextValue + + val valueToken = memberContext.INTEGER() + if (valueToken != null) { + value = parseInt(valueToken) + } + + if (!values.add(value)) { + errorReporter.error(locationOf(memberContext), "duplicate enum value: $value") + continue + } + + nextValue = value + 1 + + val element = EnumMemberElement( + location = locationOf(memberContext), + name = memberContext.IDENTIFIER().text, + value = value, + documentation = formatJavadoc(memberContext), + annotations = annotationsFromAntlr(memberContext.annotationList())) + + members.add(element) + } + + val doc = formatJavadoc(ctx) + val element = EnumElement( + location = locationOf(ctx), + name = enumName, + documentation = doc, + annotations = annotationsFromAntlr(ctx.annotationList()), + members = members) + + enums.add(element) + } + + override fun exitStructDef(ctx: AntlrThriftParser.StructDefContext) { + val name = ctx.IDENTIFIER().text + val fields = parseFieldList(ctx.field()) + + val element = StructElement( + location = locationOf(ctx), + name = name, + fields = fields, + type = StructElement.Type.STRUCT, + documentation = formatJavadoc(ctx), + annotations = annotationsFromAntlr(ctx.annotationList())) + + structs.add(element) + } + + override fun exitUnionDef(ctx: AntlrThriftParser.UnionDefContext) { + val name = ctx.IDENTIFIER().text + val fields = parseFieldList(ctx.field()) + + var numFieldsWithDefaultValues = 0 + for (i in fields.indices) { + val element = fields[i] + if (element.requiredness == Requiredness.REQUIRED) { + val fieldContext = ctx.field(i) + errorReporter.error(locationOf(fieldContext), "unions cannot have required fields") + } + + if (element.constValue != null) { + ++numFieldsWithDefaultValues + } + } + + if (numFieldsWithDefaultValues > 1) { + errorReporter.error(locationOf(ctx), "unions can have at most one default value") + } + + val element = StructElement( + location = locationOf(ctx), + name = name, + fields = fields, + type = StructElement.Type.UNION, + documentation = formatJavadoc(ctx), + annotations = annotationsFromAntlr(ctx.annotationList())) + + unions.add(element) + } + + override fun exitExceptionDef(ctx: AntlrThriftParser.ExceptionDefContext) { + val name = ctx.IDENTIFIER().text + val fields = parseFieldList(ctx.field()) + + val element = StructElement( + location = locationOf(ctx), + name = name, + fields = fields, + type = StructElement.Type.EXCEPTION, + documentation = formatJavadoc(ctx), + annotations = annotationsFromAntlr(ctx.annotationList())) + + exceptions.add(element) + } + + private fun parseFieldList( + contexts: List, + defaultRequiredness: Requiredness = Requiredness.DEFAULT): ImmutableList { + var builder: ImmutableList.Builder = ImmutableList.builder() + val ids = mutableSetOf() + + var nextValue = 1 + for (fieldContext in contexts) { + val element = parseField(nextValue, fieldContext, defaultRequiredness) + if (element != null) { + builder = builder.add(element) + + if (!ids.add(element.fieldId)) { + errorReporter.error(locationOf(fieldContext), "duplicate field ID: ${element.fieldId}") + } + + if (element.fieldId <= 0) { + errorReporter.error(locationOf(fieldContext), "field ID must be greater than zero") + } + + if (element.fieldId >= nextValue) { + nextValue = element.fieldId + 1 + } + } else { + // assert-fail here? + ++nextValue // this represents an error condition + } + } + + return builder.build() + } + + private fun parseField( + defaultValue: Int, + ctx: AntlrThriftParser.FieldContext, + defaultRequiredness: Requiredness): FieldElement? { + + val fieldId = ctx.INTEGER()?.let { parseInt(it) } ?: defaultValue + val fieldName = ctx.IDENTIFIER().text + + val requiredness = if (ctx.requiredness() != null) { + when { + ctx.requiredness().text == "required" -> Requiredness.REQUIRED + ctx.requiredness().text == "optional" -> Requiredness.OPTIONAL + else -> throw AssertionError("Unexpected requiredness value: " + ctx.requiredness().text) + } + } else { + defaultRequiredness + } + + return FieldElement( + location = locationOf(ctx), + documentation = formatJavadoc(ctx), + fieldId = fieldId, + requiredness = requiredness, + type = typeElementOf(ctx.fieldType()), + name = fieldName, + annotations = annotationsFromAntlr(ctx.annotationList()), + constValue = constValueElementOf(ctx.constValue())) + } + + override fun exitTypedef(ctx: AntlrThriftParser.TypedefContext) { + val oldType = typeElementOf(ctx.fieldType()) + + val typedef = TypedefElement( + location = locationOf(ctx), + documentation = formatJavadoc(ctx), + annotations = annotationsFromAntlr(ctx.annotationList()), + oldType = oldType, + newName = ctx.IDENTIFIER().text) + + typedefs.add(typedef) + } + + override fun exitConstDef(ctx: AntlrThriftParser.ConstDefContext) { + val constValue = constValueElementOf(ctx.constValue()) + if (constValue == null) { + errorReporter.error( + locationOf(ctx.constValue()), + "Invalid const value") + return + } + + val element = ConstElement( + location = locationOf(ctx), + documentation = formatJavadoc(ctx), + type = typeElementOf(ctx.fieldType()), + name = ctx.IDENTIFIER().text, + value = constValue) + + consts.add(element) + } + + override fun exitServiceDef(ctx: AntlrThriftParser.ServiceDefContext) { + val name = ctx.name.text + + val extendsService = if (ctx.superType != null) { + val superType = typeElementOf(ctx.superType) + + if (superType !is ScalarTypeElement) { + errorReporter.error(locationOf(ctx), "services cannot extend collections") + return + } + + superType + } else { + null + } + + val service = ServiceElement( + location = locationOf(ctx), + name = name, + functions = parseFunctionList(ctx.function()), + extendsService = extendsService, + documentation = formatJavadoc(ctx), + annotations = annotationsFromAntlr(ctx.annotationList()) + ) + + services.add(service) + } + + private fun parseFunctionList(functionContexts: List): ImmutableList { + val functions = ImmutableList.builder() + + for (ctx in functionContexts) { + val name = ctx.IDENTIFIER().text + + val returnType = if (ctx.fieldType() != null) { + typeElementOf(ctx.fieldType()) + } else { + val token = ctx.getToken(AntlrThriftLexer.VOID, 0) + ?: throw AssertionError("Function has no return type, and no VOID token - grammar error") + val loc = locationOf(token) + + // Do people actually annotation 'void'? We'll find out! + TypeElement.scalar(loc, "void", null) + } + + val isOneway = ctx.ONEWAY() != null + + val maybeThrowsList = ctx.throwsList()?.let { + parseFieldList(it.fieldList().field()) + } + + val function = FunctionElement( + location = locationOf(ctx), + oneWay = isOneway, + returnType = returnType, + name = name, + documentation = formatJavadoc(ctx), + annotations = annotationsFromAntlr(ctx.annotationList()), + params = parseFieldList(ctx.fieldList().field(), Requiredness.REQUIRED), + exceptions = maybeThrowsList ?: emptyList() + ) + + functions.add(function) + } + + return functions.build() + } + + override fun visitErrorNode(node: ErrorNode) { + errorReporter.error(locationOf(node), node.text) + } + + // region Utilities + + private fun annotationsFromAntlr(ctx: AntlrThriftParser.AnnotationListContext?): AnnotationElement? { + if (ctx == null) { + return null + } + + val annotations = mutableMapOf() + for (annotationContext in ctx.annotation()) { + val name = annotationContext.IDENTIFIER().text + annotations[name] = if (annotationContext.LITERAL() != null) { + unquote(locationOf(annotationContext.LITERAL()), annotationContext.LITERAL().text) + } else { + "true" + } + } + + return AnnotationElement(locationOf(ctx), annotations) + } + + private fun locationOf(ctx: ParserRuleContext): Location { + return locationOf(ctx.getStart()) + } + + private fun locationOf(node: TerminalNode): Location { + return locationOf(node.symbol) + } + + private fun locationOf(token: Token): Location { + val line = token.line + val col = token.charPositionInLine + 1 // Location.col is 1-based, Token.col is 0-based + return location.at(line, col) + } + + private fun unquote(location: Location, literal: String, processEscapes: Boolean = true): String { + val chars = literal.toCharArray() + val startChar = chars[0] + val endChar = chars[chars.size - 1] + + if (startChar != endChar || startChar != '\'' && startChar != '"') { + throw AssertionError("Incorrect UNESCAPED_LITERAL rule: $literal") + } + + val sb = StringBuilder(literal.length - 2) + + var i = 1 + val end = chars.size - 1 + while (i < end) { + val c = chars[i++] + + if (processEscapes && c == '\\') { + if (i == end) { + errorReporter.error(location, "Unterminated literal") + break + } + + val escape = chars[i++] + when (escape) { + 'a' -> sb.append(0x7.toChar()) + 'b' -> sb.append('\b') + 'f' -> sb.append('\u000C') + 'n' -> sb.append('\n') + 'r' -> sb.append('\r') + 't' -> sb.append('\t') + 'v' -> sb.append(0xB.toChar()) + '\\' -> sb.append('\\') + 'u' -> throw UnsupportedOperationException("unicode escapes not yet implemented") + else -> if (escape == startChar) { + sb.append(startChar) + } else { + errorReporter.error(location, "invalid escape character: $escape") + } + } + } else { + sb.append(c) + } + } + + return sb.toString() + } + + private fun typeElementOf(context: AntlrThriftParser.FieldTypeContext): TypeElement { + if (context.baseType() != null) { + if (context.baseType().text == "slist") { + errorReporter.error(locationOf(context), "slist is unsupported; use list instead") + } + + return TypeElement.scalar( + locationOf(context), + context.baseType().text, + annotationsFromAntlr(context.annotationList())) + } + + if (context.IDENTIFIER() != null) { + return TypeElement.scalar( + locationOf(context), + context.IDENTIFIER().text, + annotationsFromAntlr(context.annotationList())) + } + + if (context.containerType() != null) { + val containerContext = context.containerType() + if (containerContext.mapType() != null) { + val keyType = typeElementOf(containerContext.mapType().key) + val valueType = typeElementOf(containerContext.mapType().value) + return TypeElement.map( + locationOf(containerContext.mapType()), + keyType, + valueType, + annotationsFromAntlr(context.annotationList())) + } + + if (containerContext.setType() != null) { + return TypeElement.set( + locationOf(containerContext.setType()), + typeElementOf(containerContext.setType().fieldType()), + annotationsFromAntlr(context.annotationList())) + } + + if (containerContext.listType() != null) { + return TypeElement.list( + locationOf(containerContext.listType()), + typeElementOf(containerContext.listType().fieldType()), + annotationsFromAntlr(context.annotationList())) + } + + throw AssertionError("Unexpected container type - grammar error!") + } + + throw AssertionError("Unexpected type - grammar error!") + } + + private fun constValueElementOf(ctx: AntlrThriftParser.ConstValueContext?): ConstValueElement? { + if (ctx == null) { + return null + } + + if (ctx.INTEGER() != null) { + try { + val value = parseLong(ctx.INTEGER()) + + return ConstValueElement.integer(locationOf(ctx), ctx.INTEGER().text, value) + } catch (e: NumberFormatException) { + throw AssertionError("Invalid integer accepted by ANTLR grammar: " + ctx.INTEGER().text) + } + + } + + if (ctx.DOUBLE() != null) { + val text = ctx.DOUBLE().text + + try { + val value = java.lang.Double.parseDouble(text) + return ConstValueElement.real(locationOf(ctx), ctx.DOUBLE().text, value) + } catch (e: NumberFormatException) { + throw AssertionError("Invalid double accepted by ANTLR grammar: $text") + } + + } + + if (ctx.LITERAL() != null) { + val text = unquote(locationOf(ctx.LITERAL() as TerminalNode), ctx.LITERAL().text) + return ConstValueElement.literal(locationOf(ctx), ctx.LITERAL().text, text) + } + + if (ctx.IDENTIFIER() != null) { + val id = ctx.IDENTIFIER().text + return ConstValueElement.identifier(locationOf(ctx), ctx.IDENTIFIER().text, id) + } + + if (ctx.constList() != null) { + val values = ImmutableList.builder() + for (valueContext in ctx.constList().constValue()) { + values.add(constValueElementOf(valueContext)!!) + } + return ConstValueElement.list(locationOf(ctx), ctx.constList().text, values.build()) + } + + if (ctx.constMap() != null) { + val values = ImmutableMap.builder() + for (entry in ctx.constMap().constMapEntry()) { + val key = constValueElementOf(entry.key) + val value = constValueElementOf(entry.value) + values.put(key!!, value!!) + } + return ConstValueElement.map(locationOf(ctx), ctx.constMap().text, values.build()) + } + + throw AssertionError("unreachable") + } + + private fun formatJavadoc(context: ParserRuleContext): String { + return formatJavadoc( + getLeadingComments(context.getStart()) + getTrailingComments(context.getStop())) + } + + private fun getLeadingComments(token: Token): List { + val hiddenTokens = tokenStream.getHiddenTokensToLeft(token.tokenIndex, Lexer.HIDDEN) + + return hiddenTokens?.filter { + it.isComment && !trailingDocTokenIndexes.get(it.tokenIndex) + } ?: emptyList() + } + + /** + * Read comments following the given token, until the first newline is encountered. + * + * INVARIANT: + * Assumes that the parse tree is being walked top-down, left to right! + * + * Trailing-doc tokens are marked as such, so that subsequent searches for "leading" + * doc don't grab tokens already used as "trailing" doc. If the walk order is *not* + * top-down, left-to-right, then the assumption underpinning the separation of leading + * and trailing comments is broken. + * + * @param endToken the token from which to search for trailing comment tokens. + * @return a list, possibly empty, of all trailing comment tokens. + */ + private fun getTrailingComments(endToken: Token): List { + val hiddenTokens = tokenStream.getHiddenTokensToRight(endToken.tokenIndex, Lexer.HIDDEN) + + if (hiddenTokens != null && hiddenTokens.isNotEmpty()) { + val maybeTrailingDoc = hiddenTokens.first() // only one trailing comment is possible + + if (maybeTrailingDoc.isComment) { + trailingDocTokenIndexes.set(maybeTrailingDoc.tokenIndex) + return listOf(maybeTrailingDoc) + } + } + + return emptyList() + } + + companion object { + // A number of tokens that should comfortably accommodate most input files + // without wildly re-allocating. Estimated based on the ClientTestThrift + // and TestThrift files, which contain around ~1200 tokens each. + private const val INITIAL_BITSET_CAPACITY = 2048 + + + } + + // endregion +} + +private val Token.isComment: Boolean + get() { + return when (this.type) { + AntlrThriftLexer.SLASH_SLASH_COMMENT, + AntlrThriftLexer.HASH_COMMENT, + AntlrThriftLexer.MULTILINE_COMMENT -> true + + else -> false + } + } + +private fun formatJavadoc(commentTokens: List): String { + val sb = StringBuilder() + + for (token in commentTokens) { + val text = token.text + when (token.type) { + AntlrThriftLexer.SLASH_SLASH_COMMENT -> formatSingleLineComment(sb, text, "//") + + AntlrThriftLexer.HASH_COMMENT -> formatSingleLineComment(sb, text, "#") + + AntlrThriftLexer.MULTILINE_COMMENT -> formatMultilineComment(sb, text) + + else -> { + // wat + val symbolicName = AntlrThriftParser.VOCABULARY.getSymbolicName(token.type) + val literalName = AntlrThriftParser.VOCABULARY.getLiteralName(token.type) + val tokenNumber = "${token.type}" + error("Unexpected comment-token type: ${symbolicName ?: literalName ?: tokenNumber}") + } + } + } + + return sb.toString().trim { it <= ' ' }.let { doc -> + if (doc.isNotEmpty() && !doc.endsWith("\n")) { + doc + "\n" + } else { + doc + } + } +} + +private fun formatSingleLineComment(sb: StringBuilder, text: String, prefix: String) { + var start = prefix.length + var end = text.length + + while (start < end && Character.isWhitespace(text[start])) { + ++start + } + + while (end > start && Character.isWhitespace(text[end - 1])) { + --end + } + + if (start != end) { + sb.append(text.substring(start, end)) + } + + sb.append("\n") +} + +private fun formatMultilineComment(sb: StringBuilder, text: String) { + val chars = text.toCharArray() + var pos = "/*".length + val length = chars.size + var isStartOfLine = true + + while (pos + 1 < length) { + val c = chars[pos] + if (c == '*' && chars[pos + 1] == '/') { + sb.append("\n") + return + } + + if (c == '\n') { + sb.append(c) + isStartOfLine = true + } else if (!isStartOfLine) { + sb.append(c) + } else if (c == '*') { + // skip a single subsequent space, if it exists + if (chars[pos + 1] == ' ') { + pos += 1 + } + + isStartOfLine = false + } else if (!Character.isWhitespace(c)) { + sb.append(c) + isStartOfLine = false + } + ++pos + } +} + +private fun parseInt(node: TerminalNode): Int { + return parseInt(node.symbol) +} + +private fun parseInt(token: Token): Int { + var text = token.text + + var radix = 10 + if (text.startsWith("0x") || text.startsWith("0X")) { + radix = 16 + text = text.substring(2) + } + + return Integer.parseInt(text, radix) +} + +private fun parseLong(node: TerminalNode): Long = parseLong(node.symbol) + +private fun parseLong(token: Token): Long { + val text: String + val radix: Int + + if (token.text.startsWith("0x", ignoreCase = true)) { + text = token.text.substring(2) + radix = 16 + } else { + text = token.text + radix = 10 + } + + return java.lang.Long.parseLong(text, radix) +} diff --git a/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ThriftParser.kt b/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ThriftParser.kt new file mode 100644 index 0000000..742809d --- /dev/null +++ b/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ThriftParser.kt @@ -0,0 +1,65 @@ +/* + * Thrifty + * + * Copyright (c) Microsoft Corporation + * + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING + * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, + * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. + * + * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. + */ +package com.microsoft.thrifty.schema.parser + +import com.google.common.base.Joiner +import com.microsoft.thrifty.schema.ErrorReporter +import com.microsoft.thrifty.schema.Location +import com.microsoft.thrifty.schema.antlr.AntlrThriftLexer +import com.microsoft.thrifty.schema.antlr.AntlrThriftParser +import org.antlr.v4.runtime.CharStreams +import org.antlr.v4.runtime.CommonTokenStream +import org.antlr.v4.runtime.tree.ParseTreeWalker +import java.util.Locale + +object ThriftParser { + + /** + * Parse the given Thrift [text], using the given [location] to anchor + * parsed elements within the file. + * + * @param location the [Location] of the file containing the given [text]. + * @param text the Thrift text to be parsed. + * @param reporter an [ErrorReporter] to which parse errors will be reported. + * @return a [ThriftFileElement] containing all Thrift elements in the input text. + */ + @JvmStatic + @JvmOverloads + fun parse(location: Location, text: String, reporter: ErrorReporter = ErrorReporter()): ThriftFileElement { + val charStream = CharStreams.fromString(text, location.path()) + val lexer = AntlrThriftLexer(charStream) + val tokenStream = CommonTokenStream(lexer) + val antlrParser = AntlrThriftParser(tokenStream) + val documentParseTree = antlrParser.document() + + val thriftListener = ThriftListener(tokenStream, reporter, location) + + ParseTreeWalker.DEFAULT.walk(thriftListener, documentParseTree) + + if (reporter.hasError()) { + val errorReports = Joiner.on('\n').join(reporter.formattedReports()) + val message = String.format(Locale.US, "Syntax errors in %s:\n%s", location, errorReports) + throw IllegalStateException(message) + } + + return thriftListener.buildFileElement() + } +} \ No newline at end of file diff --git a/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ThriftyParserPlugins.kt b/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ThriftyParserPlugins.kt new file mode 100644 index 0000000..4e500c2 --- /dev/null +++ b/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/ThriftyParserPlugins.kt @@ -0,0 +1,89 @@ +/* + * Thrifty + * + * Copyright (c) Microsoft Corporation + * + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING + * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, + * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. + * + * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. + */ +package com.microsoft.thrifty.schema.parser + +import java.util.UUID + +object ThriftyParserPlugins { + private val DEFAULT_UUID_PROVIDER: UUIDProvider = object : UUIDProvider { + override fun call(): UUID = UUID.randomUUID() + } + + @Volatile + private var uuidProvider = DEFAULT_UUID_PROVIDER + + /** + * Prevents changing the plugins. + */ + @Volatile + private var lockdown: Boolean = false + + /** + * Prevents changing the plugins from then on. + * + * + * This allows container-like environments to prevent client messing with plugins. + */ + @JvmStatic fun lockdown() { + lockdown = true + } + + /** + * Returns true if the plugins were locked down. + * + * @return true if the plugins were locked down + */ + @JvmStatic fun isLockdown(): Boolean { + return lockdown + } + + /** + * @param uuidProvider the provider to use for generating [UUID]s for elements. + */ + @JvmStatic fun setUUIDProvider(uuidProvider: UUIDProvider) { + if (lockdown) { + throw IllegalStateException("Plugins can't be changed anymore") + } + ThriftyParserPlugins.uuidProvider = uuidProvider + } + + /** + * @return a [UUID] as dictated by [uuidProvider]. Default is random UUIDs. + */ + @JvmStatic fun createUUID(): UUID { + return uuidProvider.call() + } + + @JvmStatic fun reset() { + uuidProvider = DEFAULT_UUID_PROVIDER + } + + /** + * A simple provider interface for creating [UUID]s. + */ + interface UUIDProvider { + + /** + * @return a [UUID]. + */ + fun call(): UUID + } +} \ No newline at end of file diff --git a/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/TypeElements.kt b/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/TypeElements.kt new file mode 100644 index 0000000..053b2cc --- /dev/null +++ b/thrifty-schema/src/main/kotlin/com/microsoft/thrifty/schema/parser/TypeElements.kt @@ -0,0 +1,250 @@ +/* + * Thrifty + * + * Copyright (c) Microsoft Corporation + * + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING + * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, + * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. + * + * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. + */ +package com.microsoft.thrifty.schema.parser + +import com.microsoft.thrifty.schema.Location + +/** + * Represents a reference to a named type. + * + * Types can be scalar (numbers, strings, identifiers, etc) or collections. + */ +sealed class TypeElement( + /** + * The location of the text corresponding to this element. + */ + @get:JvmName("location") + val location: Location, + + /** + * The name of the type referenced by this element. + */ + @get:JvmName("name") + val name: String, + + /** + * The annotations associated with this type reference, if any. + */ + @get:JvmName("annotations") + val annotations: AnnotationElement? = null +) { + + companion object { + /** + * Creates and returns a scalar [TypeElement]. + * + * @param location the location of the text corresponding to the scalar. + * @param name the name of the scalar-type reference. + * @param annotations the annotations associated with the reference, if any. + * @return a [ScalarTypeElement]. + */ + @JvmStatic + fun scalar(location: Location, name: String, annotations: AnnotationElement? = null) = + ScalarTypeElement(location, name, annotations) + + /** + * Creates and returns a list [TypeElement]. + * + * @param location the location of the text corresponding to the reference. + * @param elementType the type of element contained by the list type. + * @param annotations the annotations associated with the reference, if any. + * @return a [ListTypeElement]. + */ + @JvmStatic + fun list(location: Location, elementType: TypeElement, annotations: AnnotationElement? = null) = + ListTypeElement(elementType, location, annotations) + + /** + * Creates and returns a set [TypeElement]. + * + * @param location the location of the text corresponding to the reference. + * @param elementType the type of element contained by the set type. + * @param annotations the annotations associated with the reference, if any. + * @return a [SetTypeElement]. + */ + @JvmStatic + fun set(location: Location, elementType: TypeElement, annotations: AnnotationElement? = null) = + SetTypeElement(elementType, location, annotations) + + /** + * Creates and returns a map [TypeElement]. + * + * @param location the location of the text corresponding to the reference. + * @param keyType the ley-type of the map. + * @param valueType the value-type of the map. + * @param annotations the annotations associated with the reference, if any. + * @return a [MapTypeElement]. + */ + @JvmStatic + fun map(location: Location, + keyType: TypeElement, + valueType: TypeElement, + annotations: AnnotationElement? = null) = MapTypeElement(keyType, valueType, location, annotations) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TypeElement + + if (location != other.location) return false + if (name != other.name) return false + if (annotations != other.annotations) return false + + return true + } + + override fun hashCode(): Int { + var result = location.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + (annotations?.hashCode() ?: 0) + return result + } +} + +/** + * Represents a reference to a scalar type. + */ +class ScalarTypeElement( + location: Location, + name: String, + annotations: AnnotationElement? +) : TypeElement(location, name, annotations) { + + override fun toString(): String { + return "ScalarTypeElement{location=$location, name=$name, annotations=$annotations}" + } +} + +/** + * Represents a reference to a set-type. + */ +class SetTypeElement( + /** + * A reference to the type of element contained within this set type. + */ + @get:JvmName("elementType") + val elementType: TypeElement, + location: Location, + annotations: AnnotationElement? = null +) : TypeElement(location, "set<${elementType.name}>", annotations) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SetTypeElement) return false + if (!super.equals(other)) return false + + if (elementType != other.elementType) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + elementType.hashCode() + return result + } + + override fun toString(): String { + return "SetTypeElement{location=$location, name=$name, annotations=$annotations, elementType=$elementType}" + } +} + +/** + * Represents a reference to a list-type. + */ +class ListTypeElement( + /** + * A reference to the type of element contained within this list type. + */ + @get:JvmName("elementType") + val elementType: TypeElement, + location: Location, + annotations: AnnotationElement? = null +) : TypeElement(location, "list<${elementType.name}>", annotations) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ListTypeElement) return false + if (!super.equals(other)) return false + + if (elementType != other.elementType) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + elementType.hashCode() + return result + } + + override fun toString(): String { + return "ListTypeElement{location=$location, name=$name, annotations=$annotations, elementType=$elementType}" + } +} + +/** + * Represents a reference to a map-type. + * + * A map-type is a typical key-value lookup, a.k.a. associative array, + * dictionary, etc. + */ +class MapTypeElement( + /** + * A reference to the type serving as the key-type of this map type. + */ + @get:JvmName("keyType") + val keyType: TypeElement, + + /** + * A reference to the type serving as the value-type of this map type. + */ + @get:JvmName("valueType") + val valueType: TypeElement, + + location: Location, + annotations: AnnotationElement? = null +) : TypeElement(location, "map<${keyType.name}, ${valueType.name}>", annotations) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is MapTypeElement) return false + if (!super.equals(other)) return false + + if (keyType != other.keyType) return false + if (valueType != other.valueType) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + keyType.hashCode() + result = 31 * result + valueType.hashCode() + return result + } + + override fun toString(): String { + return "MapTypeElement{location=$location, name=$name, annotations=$annotations, keyType=$keyType, valueType=$valueType}" + } +} \ No newline at end of file diff --git a/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/ConstantTest.java b/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/ConstantTest.java index a88c83d..3311bcd 100644 --- a/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/ConstantTest.java +++ b/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/ConstantTest.java @@ -171,7 +171,7 @@ public class ConstantTest { @Test public void enumWithMember() { ImmutableList.Builder members = ImmutableList.builder(); - members.add(new EnumMember(EnumMemberElement.builder(loc).name("TEST").value(1).build())); + members.add(new EnumMember(new EnumMemberElement(loc, "TEST", 1))); EnumType et = mock(EnumType.class); when(et.name()).thenReturn("TestEnum"); @@ -185,7 +185,7 @@ public class ConstantTest { @Test public void enumWithNonMemberIdentifier() { ImmutableList.Builder members = ImmutableList.builder(); - members.add(new EnumMember(EnumMemberElement.builder(loc).name("TEST").value(1).build())); + members.add(new EnumMember(new EnumMemberElement(loc, "TEST", 1))); EnumType et = mock(EnumType.class); when(et.name()).thenReturn("TestEnum"); @@ -206,7 +206,7 @@ public class ConstantTest { @Test public void unqualifiedEnumMember() { ImmutableList.Builder members = ImmutableList.builder(); - members.add(new EnumMember(EnumMemberElement.builder(loc).name("TEST").value(1).build())); + members.add(new EnumMember(new EnumMemberElement(loc, "TEST", 1))); EnumType et = mock(EnumType.class); when(et.name()).thenReturn("TestEnum"); @@ -252,7 +252,7 @@ public class ConstantTest { @Test public void typedefOfEnum() { - EnumMember member = new EnumMember(EnumMemberElement.builder(loc).name("FOO").value(1).build()); + EnumMember member = new EnumMember(new EnumMemberElement(loc, "FOO", 1)); ImmutableList members = ImmutableList.of(member); EnumType et = mock(EnumType.class); @@ -273,7 +273,7 @@ public class ConstantTest { @Test public void typedefOfWrongEnum() { - EnumMember member = new EnumMember(EnumMemberElement.builder(loc).name("FOO").value(1).build()); + EnumMember member = new EnumMember(new EnumMemberElement(loc, "FOO", 1)); ImmutableList members = ImmutableList.of(member); EnumType et = mock(EnumType.class); @@ -282,7 +282,7 @@ public class ConstantTest { when(et.getTrueType()).thenReturn(et); when(et.isEnum()).thenReturn(true); - EnumMember wrongMember = new EnumMember(EnumMemberElement.builder(loc).name("BAR").value(2).build()); + EnumMember wrongMember = new EnumMember(new EnumMemberElement(loc, "BAR", 2)); EnumType wt = mock(EnumType.class); when(wt.name()).thenReturn("DifferentEnum"); diff --git a/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/EnumTypeTest.java b/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/EnumTypeTest.java index 8c88470..b4ef226 100644 --- a/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/EnumTypeTest.java +++ b/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/EnumTypeTest.java @@ -15,8 +15,6 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class EnumTypeTest { - @Mock EnumElement element; - @Mock ThriftType type; @Mock Program program; ImmutableMap namespaces; @@ -27,19 +25,14 @@ public class EnumTypeTest { namespaces = ImmutableMap.of(); when(program.namespaces()).thenReturn(namespaces); - when(element.name()).thenReturn("name"); - when(element.members()).thenReturn(ImmutableList.of()); Location location = Location.get("", ""); - EnumMemberElement memberElement = EnumMemberElement.builder(location) - .name("FOO") - .value(1) - .build(); + EnumMemberElement memberElement = new EnumMemberElement(location, "FOO", 1); - EnumElement element = EnumElement.builder(location) - .name("AnEnum") - .members(ImmutableList.of(memberElement)) - .build(); + EnumElement element = new EnumElement( + location, + "AnEnum", + ImmutableList.of(memberElement)); enumType = new EnumType(program, element); } diff --git a/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/FieldTest.java b/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/FieldTest.java index bac58b4..f83e172 100644 --- a/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/FieldTest.java +++ b/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/FieldTest.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableMap; import com.microsoft.thrifty.schema.parser.AnnotationElement; import com.microsoft.thrifty.schema.parser.FieldElement; import com.microsoft.thrifty.schema.parser.TypeElement; +import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -35,11 +36,30 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; public class FieldTest { + private Location location; + private int fieldId; + private String fieldName; + private TypeElement fieldType; + private Requiredness requiredness; + private AnnotationElement annotations; + private String documentation; + + @Before + public void setup() { + location = Location.get("", ""); + fieldId = 1; + fieldName = "foo"; + fieldType = TypeElement.scalar(location, "i32", null); + requiredness = Requiredness.DEFAULT; + annotations = null; + documentation = ""; + } + @Test public void requiredFields() { - FieldElement element = fieldBuilder() - .requiredness(Requiredness.REQUIRED) - .build(); + requiredness = Requiredness.REQUIRED; + FieldElement element = field(); + Field field = new Field(element); assertTrue(field.required()); assertFalse(field.optional()); @@ -47,9 +67,8 @@ public class FieldTest { @Test public void optionalFields() { - FieldElement element = fieldBuilder() - .requiredness(Requiredness.OPTIONAL) - .build(); + requiredness = Requiredness.OPTIONAL; + FieldElement element = field(); Field field = new Field(element); assertFalse(field.required()); assertTrue(field.optional()); @@ -57,9 +76,7 @@ public class FieldTest { @Test public void defaultFields() { - FieldElement element = fieldBuilder() - .requiredness(Requiredness.DEFAULT) - .build(); + FieldElement element = field(); Field field = new Field(element); assertFalse(field.required()); assertFalse(field.optional()); @@ -67,7 +84,7 @@ public class FieldTest { @Test public void unredactedAndUnobfuscatedByDefault() { - FieldElement element = fieldBuilder().build(); + FieldElement element = field(); Field field = new Field(element); assertFalse(field.isRedacted()); assertFalse(field.isObfuscated()); @@ -75,9 +92,8 @@ public class FieldTest { @Test public void redactedByThriftAnnotation() { - FieldElement element = fieldBuilder() - .annotations(annotation("thrifty.redacted")) - .build(); + annotations = annotation("thrifty.redacted"); + FieldElement element = field(); Field field = new Field(element); assertTrue(field.isRedacted()); @@ -85,9 +101,8 @@ public class FieldTest { @Test public void redactedByShortThriftAnnotation() { - FieldElement element = fieldBuilder() - .annotations(annotation("redacted")) - .build(); + annotations = annotation("redacted"); + FieldElement element = field(); Field field = new Field(element); assertTrue(field.isRedacted()); @@ -95,9 +110,8 @@ public class FieldTest { @Test public void redactedByJavadocAnnotation() { - FieldElement element = fieldBuilder() - .documentation("/** @redacted */") - .build(); + documentation = "/** @redacted */"; + FieldElement element = field(); Field field = new Field(element); assertTrue(field.isRedacted()); @@ -105,9 +119,8 @@ public class FieldTest { @Test public void obfuscatedByThriftAnnotation() { - FieldElement element = fieldBuilder() - .annotations(annotation("thrifty.obfuscated")) - .build(); + annotations = annotation("thrifty.obfuscated"); + FieldElement element = field(); Field field = new Field(element); assertTrue(field.isObfuscated()); @@ -115,9 +128,8 @@ public class FieldTest { @Test public void obfuscatedByShortThriftAnnotation() { - FieldElement element = fieldBuilder() - .annotations(annotation("obfuscated")) - .build(); + annotations = annotation("obfuscated"); + FieldElement element = field(); Field field = new Field(element); assertTrue(field.isObfuscated()); @@ -125,9 +137,8 @@ public class FieldTest { @Test public void obfuscatedByJavadocAnnotation() { - FieldElement element = fieldBuilder() - .documentation("/** @obfuscated */") - .build(); + documentation = "/** @obfuscated */"; + FieldElement element = field(); Field field = new Field(element); assertTrue(field.isObfuscated()); @@ -135,7 +146,7 @@ public class FieldTest { @Test public void builderCreatesCorrectField() { - FieldElement fieldElement = mock(FieldElement.class); + FieldElement fieldElement = field(); Field field = new Field(fieldElement); ImmutableMap annotations = ImmutableMap.of(); @@ -160,14 +171,18 @@ public class FieldTest { } private AnnotationElement annotation(String name) { - return AnnotationElement.create(Location.get("", ""), Collections.singletonMap(name, "true")); + return new AnnotationElement(Location.get("", ""), Collections.singletonMap(name, "true")); } - private FieldElement.Builder fieldBuilder() { - Location location = Location.get("", ""); - return FieldElement.builder(location) - .fieldId(1) - .name("foo") - .type(TypeElement.scalar(location, "i32", null)); + private FieldElement field() { + return new FieldElement( + location, + fieldId, + fieldType, + fieldName, + requiredness, + documentation, + null, // const value + annotations); } } \ No newline at end of file diff --git a/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/parser/ThriftParserTest.java b/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/parser/ThriftParserTest.java deleted file mode 100644 index 1faf71d..0000000 --- a/thrifty-schema/src/test/java/com/microsoft/thrifty/schema/parser/ThriftParserTest.java +++ /dev/null @@ -1,1238 +0,0 @@ -/* - * Thrifty - * - * Copyright (c) Microsoft Corporation - * - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the License); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING - * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, - * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. - * - * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. - */ -package com.microsoft.thrifty.schema.parser; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.microsoft.thrifty.schema.ErrorReporter; -import com.microsoft.thrifty.schema.Location; -import com.microsoft.thrifty.schema.NamespaceScope; -import com.microsoft.thrifty.schema.Requiredness; -import okio.Okio; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.io.InputStream; -import java.util.Arrays; -import java.util.UUID; - -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isEmptyString; -import static org.junit.Assert.*; - -public class ThriftParserTest { - - private static final UUID TEST_UUID = UUID.fromString("ecafa042-668a-4403-a6d3-70983866ffbe"); - - @Before - public void setup() { - ThriftyParserPlugins.setUUIDProvider(() -> TEST_UUID); - } - - @After - public void tearDown() { - ThriftyParserPlugins.reset(); - } - - @Test - public void namespaces() { - String thrift = - "namespace java com.microsoft.thrifty.parser\n" + - "namespace cpp microsoft.thrifty\n" + - "namespace * microsoft.thrifty\n" + - "php_namespace 'single_quoted_namespace'\n" + - "php_namespace \"double_quoted_namespace\""; - - Location location = Location.get("", "namespaces.thrift"); - ThriftFileElement file = parse(thrift, location); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .namespaces(ImmutableList.builder() - .add(NamespaceElement.builder(location.at(1, 1)) - .scope(NamespaceScope.JAVA) - .namespace("com.microsoft.thrifty.parser") - .build()) - .add(NamespaceElement.builder(location.at(2, 1)) - .scope(NamespaceScope.CPP) - .namespace("microsoft.thrifty") - .build()) - .add(NamespaceElement.builder(location.at(3, 1)) - .scope(NamespaceScope.ALL) - .namespace("microsoft.thrifty") - .build()) - .add(NamespaceElement.builder(location.at(4, 1)) - .scope(NamespaceScope.PHP) - .namespace("single_quoted_namespace") - .build()) - .add(NamespaceElement.builder(location.at(5, 1)) - .scope(NamespaceScope.PHP) - .namespace("double_quoted_namespace") - .build()) - .build()) - .build(); - - assertThat(file, equalTo(expected)); - } - - @Test - public void includes() { - String thrift = - "include 'inc/common.thrift'\n" + - "include \".././parent.thrift\"\n" + - "cpp_include 'inc/boost.hpp'\n" + - "\n" + - "namespace * microsoft"; - - Location location = Location.get("", "includes.thrift"); - ThriftFileElement expected = ThriftFileElement.builder(location) - .includes(ImmutableList.builder() - .add(IncludeElement.create(location.at(1, 1), false, "inc/common.thrift")) - .add(IncludeElement.create(location.at(2, 1), false, ".././parent.thrift")) - .add(IncludeElement.create(location.at(3, 1), true, "inc/boost.hpp")) - .build()) - .namespaces(ImmutableList.builder() - .add(NamespaceElement.builder(location.at(5, 1)) - .scope(NamespaceScope.ALL) - .namespace("microsoft") - .build()) - .build()) - .build(); - - assertThat(parse(thrift, location), equalTo(expected)); - } - - @Test - public void simpleTypedefs() { - String thrift = - "typedef i32 MyInt\n" + - "typedef string MyString\n" + - "typedef binary PrivateKey\n"; - - Location location = Location.get("", "typedefs.thrift"); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .typedefs(ImmutableList.builder() - .add(TypedefElement.builder(location.at(1, 1)) - .oldType(TypeElement.scalar(location.at(1, 9), "i32", null)) - .newName("MyInt") - .build()) - .add(TypedefElement.builder(location.at(2, 1)) - .oldType(TypeElement.scalar(location.at(2, 9), "string", null)) - .newName("MyString") - .build()) - .add(TypedefElement.builder(location.at(3, 1)) - .oldType(TypeElement.scalar(location.at(3, 9), "binary", null)) - .newName("PrivateKey") - .build()) - .build()) - .build(); - - assertThat(parse(thrift, location), equalTo(expected)); - } - - @Test - public void containerTypedefs() { - String thrift = - "typedef list IntList\n" + - "typedef set Names\n" + - "typedef map < i16,set > BlobMap\n"; - - Location location = Location.get("", "containerTypedefs.thrift"); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .typedefs(ImmutableList.builder() - .add(TypedefElement.builder(location.at(1, 1)) - .oldType(TypeElement.list( - location.at(1, 9), - TypeElement.scalar(location.at(1, 14), "i32", null), null)) - .newName("IntList") - .build()) - .add(TypedefElement.builder(location.at(2, 1)) - .oldType(TypeElement.set( - location.at(2, 9), - TypeElement.scalar(location.at(2, 13), "string", null), - null)) - .newName("Names") - .build()) - .add(TypedefElement.builder(location.at(3, 1)) - .oldType(TypeElement.map( - location.at(3, 9), - TypeElement.scalar(location.at(3, 15), "i16", null), - TypeElement.set( - location.at(3, 19), - TypeElement.scalar(location.at(3, 23), "binary", null), - null), - null)) - .newName("BlobMap") - .build()) - .build()) - .build(); - - assertThat(parse(thrift, location), equalTo(expected)); - } - - @Test - public void emptyStruct() { - String thrift = "struct Empty {}"; - Location location = Location.get("", "empty.thrift"); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .structs(ImmutableList.builder() - .add(StructElement.builder(location.at(1, 1)) - .name("Empty") - .type(StructElement.Type.STRUCT) - .fields(ImmutableList.of()) - .build()) - .build()) - .build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - public void simpleStruct() { - String thrift = "struct Simple {\n" + - " /** This field is optional */\n" + - " 1:i32 foo,\n" + - " // This next field is required\n" + - " 2: required string bar // and has trailing doc\n" + - "}"; - - Location location = Location.get("", "simple.thrift"); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .structs(ImmutableList.builder() - .add(StructElement.builder(location.at(1, 1)) - .name("Simple") - .type(StructElement.Type.STRUCT) - .fields(ImmutableList.builder() - .add(FieldElement.builder(location.at(3, 3)) - .name("foo") - .fieldId(1) - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(3, 5), "i32", null)) - .documentation("This field is optional\n") - .build()) - .add(FieldElement.builder(location.at(5, 3)) - .name("bar") - .fieldId(2) - .requiredness(Requiredness.REQUIRED) - .type(TypeElement.scalar(location.at(5, 15), "string", null)) - .documentation("This next field is required\nand has trailing doc\n") - .build()) - .build()) - .build()) - .build()) - .build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - public void trailingFieldDoc() { - String thrift = "" + - "// This struct demonstrates trailing comments\n" + - "struct TrailingDoc {\n" + - " 1: required string standard, // cpp-style\n" + - " 2: required string python, # py-style\n" + - " 3: optional binary knr; /** K&R-style **/\n" + - "}"; - - Location location = Location.get("", "trailing.thrift"); - ThriftFileElement expected = ThriftFileElement.builder(location) - .structs(ImmutableList.builder() - .add(StructElement.builder(location.at(2, 1)) - .documentation("This struct demonstrates trailing comments\n") - .name("TrailingDoc") - .type(StructElement.Type.STRUCT) - .fields(ImmutableList.builder() - .add(FieldElement.builder(location.at(3, 3)) - .fieldId(1) - .requiredness(Requiredness.REQUIRED) - .type(TypeElement.scalar(location.at(3, 15), "string", null)) - .name("standard") - .documentation("cpp-style\n") - .build()) - .add(FieldElement.builder(location.at(4, 3)) - .fieldId(2) - .requiredness(Requiredness.REQUIRED) - .type(TypeElement.scalar(location.at(4, 15), "string", null)) - .name("python") - .documentation("py-style\n") - .build()) - .add(FieldElement.builder(location.at(5, 3)) - .fieldId(3) - .requiredness(Requiredness.OPTIONAL) - .type(TypeElement.scalar(location.at(5, 15), "binary", null)) - .name("knr") - .documentation("K&R-style *\n") - .build()) - .build()) - .build()) - .build()) - .build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - public void duplicateFieldIds() { - String thrift = "struct Nope {\n" + - "1: string foo;\n" + - "1: string bar;\n" + - "}"; - - try { - parse(thrift, Location.get("", "duplicateIds.thrift")); - fail("Structs with duplicate field IDs should fail to parse"); - } catch (IllegalStateException e) { - assertThat(e.getMessage(), containsString("duplicate field ID:")); - } - } - - @Test - public void implicitDuplicateFieldIds() { - String thrift = "struct StillNope {\n" + - "string foo;\n" + - "string bar;\n" + - "1: bytes baz\n" + - "}"; - - try { - parse(thrift, Location.get("", "duplicateImplicitIds.thrift")); - fail("Structs with duplicate implicit field IDs should fail to parse"); - } catch (IllegalStateException e) { - assertThat(e.getMessage(), containsString("duplicate field ID: 1")); - } - } - - @Test - public void weirdFieldPermutations() { - String thrift = "struct WeirdButLegal {\n" + - "byte minimal\n" + - "byte minimalWithSeparator,\n" + - "byte minimalWithOtherSeparator;\n" + - "required byte requiredWithoutSeparator\n" + - "required byte requiredWithComma,\n" + - "required byte requiredWithSemicolon;\n" + - "optional i16 optionalWithoutSeparator\n" + - "optional i16 optionalWithComma,\n" + - "optional i16 optionalWithSemicolon;\n" + - "10: i32 implicitOptional\n" + - "11: i32 implicitOptionalWithComma,\n" + - "12: i32 implicitOptionalWithSemicolon;\n" + - "13: required i64 requiredId\n" + - "14: required i64 requiredIdWithComma,\n" + - "15: required i64 requiredIdWithSemicolon;\n" + - "}"; - - Location location = Location.get("", "weird.thrift"); - - StructElement expectedStruct = StructElement.builder(location.at(1, 1)) - .name("WeirdButLegal") - .type(StructElement.Type.STRUCT) - .fields(ImmutableList.builder() - .add(FieldElement.builder(location.at(2, 1)) - .fieldId(1) - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(2, 1), "byte", null)) - .name("minimal") - .build()) - .add(FieldElement.builder(location.at(3, 1)) - .fieldId(2) - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(3, 1), "byte", null)) - .name("minimalWithSeparator") - .build()) - .add(FieldElement.builder(location.at(4, 1)) - .fieldId(3) - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(4, 1), "byte", null)) - .name("minimalWithOtherSeparator") - .build()) - .add(FieldElement.builder(location.at(5, 1)) - .fieldId(4) - .requiredness(Requiredness.REQUIRED) - .type(TypeElement.scalar(location.at(5, 10), "byte", null)) - .name("requiredWithoutSeparator") - .build()) - .add(FieldElement.builder(location.at(6, 1)) - .fieldId(5) - .requiredness(Requiredness.REQUIRED) - .type(TypeElement.scalar(location.at(6, 10), "byte", null)) - .name("requiredWithComma") - .build()) - .add(FieldElement.builder(location.at(7, 1)) - .fieldId(6) - .requiredness(Requiredness.REQUIRED) - .type(TypeElement.scalar(location.at(7, 10), "byte", null)) - .name("requiredWithSemicolon") - .build()) - .add(FieldElement.builder(location.at(8, 1)) - .fieldId(7) - .requiredness(Requiredness.OPTIONAL) - .type(TypeElement.scalar(location.at(8, 10), "i16", null)) - .name("optionalWithoutSeparator") - .build()) - .add(FieldElement.builder(location.at(9, 1)) - .fieldId(8) - .requiredness(Requiredness.OPTIONAL) - .type(TypeElement.scalar(location.at(9, 10), "i16", null)) - .name("optionalWithComma") - .build()) - .add(FieldElement.builder(location.at(10, 1)) - .fieldId(9) - .requiredness(Requiredness.OPTIONAL) - .type(TypeElement.scalar(location.at(10, 10), "i16", null)) - .name("optionalWithSemicolon") - .build()) - .add(FieldElement.builder(location.at(11, 1)) - .fieldId(10) - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(11, 5), "i32", null)) - .name("implicitOptional") - .build()) - .add(FieldElement.builder(location.at(12, 1)) - .fieldId(11) - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(12, 5), "i32", null)) - .name("implicitOptionalWithComma") - .build()) - .add(FieldElement.builder(location.at(13, 1)) - .fieldId(12) - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(13, 5), "i32", null)) - .name("implicitOptionalWithSemicolon") - .build()) - .add(FieldElement.builder(location.at(14, 1)) - .fieldId(13) - .requiredness(Requiredness.REQUIRED) - .type(TypeElement.scalar(location.at(14, 14), "i64", null)) - .name("requiredId") - .build()) - .add(FieldElement.builder(location.at(15, 1)) - .fieldId(14) - .requiredness(Requiredness.REQUIRED) - .type(TypeElement.scalar(location.at(15, 14), "i64", null)) - .name("requiredIdWithComma") - .build()) - .add(FieldElement.builder(location.at(16, 1)) - .fieldId(15) - .requiredness(Requiredness.REQUIRED) - .type(TypeElement.scalar(location.at(16, 14), "i64", null)) - .name("requiredIdWithSemicolon") - .build()) - .build()) - .build(); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .structs(ImmutableList.of(expectedStruct)) - .build(); - - assertThat(parse(thrift, location), equalTo(expected)); - } - - @Test - public void invalidFieldIds() { - String thrift = "struct NegativeId { -1: required i32 nope }"; - try { - parse(thrift); - fail("Should not parse a struct with a negative field ID"); - } catch (IllegalStateException e) { - assertThat(e.getMessage(), containsString("field ID must be greater than zero")); - } - - thrift = "struct ZeroId {\n" + - " 0: optional i64 stillNope\n" + - "}"; - try { - parse(thrift); - fail("Should not parse a struct with a zero field ID"); - } catch (IllegalStateException e) { - assertThat(e.getMessage(), containsString("field ID must be greater than zero")); - } - } - - @Test - public void services() { - // NOTE: the service defined below is *not a legal Thrift service*. - // 'oneway' functions must return void, and may not throw. - // We don't do this level of semantic validation here. - String thrift = "" + - "service Svc {\n" + - " FooResult foo(1:FooRequest request, 2: optional FooMeta meta)\n" + - " oneway BarResult bar() throws (1:FooException foo, 2:BarException bar)\n" + - "}"; - - Location location = Location.get("", "simpleService.thrift"); - ThriftFileElement expected = ThriftFileElement.builder(location) - .services(ImmutableList.of(ServiceElement.builder(location.at(1, 1)) - .name("Svc") - .functions(ImmutableList.builder() - .add(FunctionElement.builder(location.at(2, 3)) - .name("foo") - .returnType(TypeElement.scalar(location.at(2, 3), "FooResult", null)) - .oneWay(false) - .params(ImmutableList.builder() - .add(FieldElement.builder(location.at(2, 17)) - .fieldId(1) - .requiredness(Requiredness.REQUIRED) - .name("request") - .type(TypeElement.scalar(location.at(2, 19), "FooRequest", null)) - .build()) - .add(FieldElement.builder(location.at(2, 39)) - .fieldId(2) - .requiredness(Requiredness.OPTIONAL) - .name("meta") - .type(TypeElement.scalar(location.at(2, 51), "FooMeta", null)) - .build()) - .build()) - .build()) - .add(FunctionElement.builder(location.at(3, 3)) - .name("bar") - .oneWay(true) - .returnType(TypeElement.scalar(location.at(3, 10), "BarResult", null)) - .exceptions(ImmutableList.builder() - .add(FieldElement.builder(location.at(3, 34)) - .fieldId(1) - .name("foo") - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(3, 36), "FooException", null)) - .build()) - .add(FieldElement.builder(location.at(3, 54)) - .fieldId(2) - .name("bar") - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(3, 56), "BarException", null)) - .build()) - .build()) - .build()) - .build()) - .build())) - .build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - public void serviceWithNewlineBeforeThrows() { - String thrift = "" + - "service Svc {\n" + - " void foo()\n" + - " throws (1: Blargh blah)\n" + - " i32 bar()\n" + - "}"; - - Location loc = Location.get("", "services.thrift"); - ThriftFileElement expected = ThriftFileElement.builder(loc) - .services(ImmutableList.of(ServiceElement.builder(loc.at(1, 1)) - .name("Svc") - .functions(ImmutableList.of( - FunctionElement.builder(loc.at(2, 3)) - .name("foo") - .returnType(TypeElement.scalar(loc.at(2, 3), "void", null)) - .exceptions(ImmutableList.of(FieldElement.builder(loc.at(3, 13)) - .fieldId(1) - .name("blah") - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(loc.at(3, 16), "Blargh", null)) - .build())) - .build(), - FunctionElement.builder(loc.at(4, 3)) - .name("bar") - .returnType(TypeElement.scalar(loc.at(4, 3), "i32", null)) - .build())) - .build())) - .build(); - - assertThat(parse(thrift, loc), equalTo(expected)); - } - - @Test - public void unions() { - String thrift = "" + - "union Normal {\n" + - " 2: i16 foo,\n" + - " 4: i32 bar\n" + - "}\n"; - - Location location = Location.get("", "union.thrift"); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .unions(ImmutableList.of(StructElement.builder(location.at(1, 1)) - .name("Normal") - .type(StructElement.Type.UNION) - .fields(ImmutableList.builder() - .add(FieldElement.builder(location.at(2, 3)) - .fieldId(2) - .requiredness(Requiredness.DEFAULT) - .name("foo") - .type(TypeElement.scalar(location.at(2, 6), "i16", null)) - .build()) - .add(FieldElement.builder(location.at(3, 3)) - .fieldId(4) - .requiredness(Requiredness.DEFAULT) - .name("bar") - .type(TypeElement.scalar(location.at(3, 6), "i32", null)) - .build()) - .build()) - .build())).build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - public void unionCannotHaveRequiredField() { - String thrift = "\n" + - "union Normal {\n" + - " 3: optional i16 foo,\n" + - " 5: required i32 bar\n" + - "}\n"; - - try { - parse(thrift, Location.get("", "unionWithRequired.thrift")); - fail("Union cannot have a required field"); - } catch (IllegalStateException e) { - assertThat(e.getMessage(), containsString("unions cannot have required fields")); - } - } - - @Test - public void unionCannotHaveMultipleDefaultValues() { - String thrift = "\n" + - "union Normal {\n" + - " 3: i16 foo = 1,\n" + - " 5: i32 bar = 2\n" + - "}\n"; - - try { - parse(thrift, Location.get("", "unionWithRequired.thrift")); - fail("Union cannot have a more than one default value"); - } catch (IllegalStateException e) { - assertThat(e.getMessage(), containsString("unions can have at most one default value")); - } - } - - @Test - public void unionsCanHaveOneDefaultValue() { - String thrift = "" + - "union Default {\n" + - " 1: i16 foo,\n" + - " 2: i16 bar,\n" + - " 3: i16 baz = 0x0FFF,\n" + - " 4: i16 quux\n" + - "}"; - - Location location = Location.get("", "unionWithDefault.thrift"); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .unions(ImmutableList.of(StructElement.builder(location.at(1, 1)) - .name("Default") - .type(StructElement.Type.UNION) - .fields(ImmutableList.builder() - .add(FieldElement.builder(location.at(2, 3)) - .fieldId(1) - .name("foo") - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(2, 6), "i16", null)) - .build()) - .add(FieldElement.builder(location.at(3, 3)) - .fieldId(2) - .name("bar") - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(3, 6), "i16", null)) - .build()) - .add(FieldElement.builder(location.at(4, 3)) - .fieldId(3) - .name("baz") - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(4, 6), "i16", null)) - .constValue(ConstValueElement.integer(location.at(4, 16), "0x0FFF", 0xFFF)) - .build()) - .add(FieldElement.builder(location.at(5, 3)) - .fieldId(4) - .name("quux") - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(5, 6), "i16", null)) - .build()) - .build()) - .build())) - .build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - public void simpleConst() { - String thrift = "const i64 DefaultStatusCode = 200"; - Location location = Location.get("", "simpleConst.thrift"); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .constants(ImmutableList.of(ConstElement.builder(location.at(1, 1)) - .name("DefaultStatusCode") - .type(TypeElement.scalar(location.at(1, 7), "i64", null)) - .value(ConstValueElement.integer(location.at(1, 31), "200", 200)) - .build())) - .build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - public void veryLargeConst() { - String thrift = "const i64 Yuuuuuge = 0xFFFFFFFFFF"; - Location location = Location.get("", "veryLargeConst.thrift"); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .constants(ImmutableList.of(ConstElement.builder(location.at(1, 1)) - .name("Yuuuuuge") - .type(TypeElement.scalar(location.at(1, 7), "i64", null)) - .value(ConstValueElement.integer(location.at(1, 22), "0xFFFFFFFFFF", 0xFFFFFFFFFFL)) - .build())) - .build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - public void listConst() { - String thrift = "const list Names = [\"foo\" \"bar\", \"baz\"; \"quux\"]"; - Location location = Location.get("", "listConst.thrift"); - - ConstElement element = ConstElement.builder(location.at(1, 1)) - .name("Names") - .type(TypeElement.list( - location.at(1, 7), - TypeElement.scalar(location.at(1, 12), "string", null), - null)) - .value(ConstValueElement.list( - location.at(1, 28), - "[\"foo\"\"bar\",\"baz\";\"quux\"]", - Arrays.asList( - ConstValueElement.literal(location.at(1, 29), "\"foo\"", "foo"), - ConstValueElement.literal(location.at(1, 35), "\"bar\"", "bar"), - ConstValueElement.literal(location.at(1, 42), "\"baz\"", "baz"), - ConstValueElement.literal(location.at(1, 49), "\"quux\"", "quux")))) - .build(); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .constants(ImmutableList.of(element)) - .build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - @SuppressWarnings("unchecked") - public void mapConst() { - String thrift = "const map Headers = {\n" + - " \"foo\": \"bar\",\n" + - " \"baz\": \"quux\";\n" + - "}"; - - Location location = Location.get("", "mapConst.thrift"); - ConstElement mapConst = ConstElement.builder(location.at(1, 1)) - .name("Headers") - .type(TypeElement.map( - location.at(1, 7), - TypeElement.scalar(location.at(1, 11), "string", null), - TypeElement.scalar(location.at(1, 19), "string", null), - null)) - .value(ConstValueElement.map(location.at(1, 37), - "{\"foo\":\"bar\",\"baz\":\"quux\";}", - ImmutableMap.of( - ConstValueElement.literal(location.at(2, 3), "\"foo\"", "foo"), - ConstValueElement.literal(location.at(2, 10), "\"bar\"", "bar"), - - ConstValueElement.literal(location.at(3, 3), "\"baz\"", "baz"), - ConstValueElement.literal(location.at(3, 10), "\"quux\"", "quux")))) - .build(); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .constants(ImmutableList.of(mapConst)) - .build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - public void structFieldWithConstValue() { - String thrift = "struct Foo {\n" + - " 100: i32 num = 1\n" + - "}"; - - Location location = Location.get("", "structWithConstValue.thrift"); - - ThriftFileElement expected = ThriftFileElement.builder(location) - .structs(ImmutableList.of(StructElement.builder(location.at(1, 1)) - .name("Foo") - .type(StructElement.Type.STRUCT) - .fields(ImmutableList.of(FieldElement.builder(location.at(2, 3)) - .fieldId(100) - .requiredness(Requiredness.DEFAULT) - .type(TypeElement.scalar(location.at(2, 8), "i32", null)) - .name("num") - .constValue(ConstValueElement.integer(location.at(2, 18), "1", 1)) - .build())) - .build())) - .build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - public void canParseOfficialTestCase() throws Exception { - ClassLoader classLoader = getClass().getClassLoader(); - InputStream stream = classLoader.getResourceAsStream("cases/TestThrift.thrift"); - String thrift = Okio.buffer(Okio.source(stream)).readUtf8(); - Location location = Location.get("cases", "TestThrift.thrift"); - - // Not crashing is good enough here. We'll be more strict with this file in the loader test. - parse(thrift, location); - } - - @Test - public void bareEnums() throws Exception { - String thrift = "enum Enum {\n" + - " FOO,\n" + - " BAR\n" + - "}"; - - Location location = Location.get("", "bareEnums.thrift"); - ThriftFileElement expected = ThriftFileElement.builder(location) - .enums(ImmutableList.of(EnumElement.builder(location.at(1, 1)) - .name("Enum") - .members(ImmutableList.builder() - .add(EnumMemberElement.builder(location.at(2, 3)) - .name("FOO") - .value(0) - .build()) - .add(EnumMemberElement.builder(location.at(3, 3)) - .name("BAR") - .value(1) - .build()) - .build()) - .build())) - .build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - public void enumWithLargeGaps() throws Exception { - String thrift = "enum Gaps {\n" + - " SMALL = 10,\n" + - " MEDIUM = 100,\n" + - " ALSO_MEDIUM,\n" + - " LARGE = 5000\n" + - "}"; - - Location location = Location.get("", "enumWithLargeGaps.thrift"); - ThriftFileElement expected = ThriftFileElement.builder(location) - .enums(ImmutableList.of(EnumElement.builder(location.at(1, 1)) - .name("Gaps") - .members(ImmutableList.builder() - .add(EnumMemberElement.builder(location.at(2, 3)) - .name("SMALL") - .value(10) - .build()) - .add(EnumMemberElement.builder(location.at(3, 3)) - .name("MEDIUM") - .value(100) - .build()) - .add(EnumMemberElement.builder(location.at(4, 3)) - .name("ALSO_MEDIUM") - .value(101) - .build()) - .add(EnumMemberElement.builder(location.at(5, 3)) - .name("LARGE") - .value(5000) - .build()) - .build()) - .build())) - .build(); - - assertThat(parse(thrift, location), is(expected)); - } - - @Test - public void defaultValuesCanClash() throws Exception { - String thrift = "enum Enum {\n" + - " FOO = 5,\n" + - " BAR = 4,\n" + - " BAZ\n" + - "}"; - - try { - parse(thrift); - fail(); - } catch (IllegalStateException e) { - assertThat(e.getMessage(), containsString("duplicate enum value")); - } - } - - @Test - public void annotationsOnNamespaces() throws Exception { - String thrift = "namespace java com.test (foo = 'bar')"; - ThriftFileElement file = parse(thrift); - NamespaceElement ns = file.namespaces().get(0); - - AnnotationElement ann = ns.annotations(); - assertNotNull(ann); - assertThat(ann.values().get("foo"), is("bar")); - } - - @Test - public void annotationsOnTypedefs() throws Exception { - String thrift = "namespace java com.test\n" + - "\n" + - "typedef i32 StatusCode (boxed = 'false');"; - - ThriftFileElement file = parse(thrift); - TypedefElement typedef = file.typedefs().get(0); - AnnotationElement ann = typedef.annotations(); - - assertNotNull(ann); - assertThat(ann.get("boxed"), is("false")); - } - - @Test - public void annotationsOnEnums() throws Exception { - String thrift = "enum Foo {} (bar = 'baz')"; - ThriftFileElement file = parse(thrift); - EnumElement e = file.enums().get(0); - AnnotationElement ann = e.annotations(); - - assertNotNull(ann); - assertThat(ann.values().get("bar"), is("baz")); - } - - @Test - @SuppressWarnings("ConstantConditions") - public void annotationsOnEnumMembers() throws Exception { - String thrift = "" + - "enum Foo {\n" + - " BAR (bar = 'abar'),\n" + - " BAZ = 1 (baz = 'abaz'),\n" + - " QUX (qux = 'aqux')\n" + - " WOO\n" + - "}"; - - ThriftFileElement file = parse(thrift); - - EnumElement anEnum = file.enums().get(0); - EnumMemberElement bar = anEnum.members().get(0); - EnumMemberElement baz = anEnum.members().get(1); - EnumMemberElement qux = anEnum.members().get(2); - EnumMemberElement woo = anEnum.members().get(3); - - assertThat(bar.annotations().get("bar"), is("abar")); - assertThat(baz.annotations().get("baz"), is("abaz")); - assertThat(qux.annotations().get("qux"), is("aqux")); - assertNull(woo.annotations()); - } - - @Test - public void annotationsOnServices() throws Exception { - String thrift = "" + - "service Svc {" + - " void foo(1: i32 bar)" + - "} (async = 'true', java.races = 'false')"; - ThriftFileElement file = parse(thrift); - ServiceElement svc = file.services().get(0); - AnnotationElement ann = svc.annotations(); - - assertNotNull(ann); - assertThat(ann.size(), is(2)); - assertThat(ann.get("async"), is("true")); - assertThat(ann.get("java.races"), is("false")); - } - - @Test - public void annotationsOnFunctions() throws Exception { - String thrift = "service Svc {\n" + - " void nothrow() (test = 'a'),\n" + - " void nosep() (test = 'b')\n" + - " i32 hasThrow() throws(1: string what) (test = 'c');\n" + - "}"; - - ThriftFileElement file = parse(thrift); - ServiceElement svc = file.services().get(0); - AnnotationElement a = svc.functions().get(0).annotations(); - AnnotationElement b = svc.functions().get(1).annotations(); - AnnotationElement c = svc.functions().get(2).annotations(); - - assertNotNull(a); - assertNotNull(b); - assertNotNull(c); - - assertThat(a.get("test"), is("a")); - assertThat(b.get("test"), is("b")); - assertThat(c.get("test"), is("c")); - } - - @Test - public void annotationsOnStructs() throws Exception { - String thrift = "" + - "struct Str {\n" + - " 1: i32 hi,\n" + - " 2: optional string there,\n" + - "} (layout = 'sequential')"; - - ThriftFileElement file = parse(thrift); - StructElement struct = file.structs().get(0); - AnnotationElement ann = struct.annotations(); - - assertNotNull(ann); - assertThat(ann.get("layout"), is("sequential")); - } - - @Test - public void annotationsOnUnions() throws Exception { - String thrift = "" + - "union Union {\n" + - " 1: i32 hi,\n" + - " 2: optional string there,\n" + - "} (layout = 'padded')"; - - ThriftFileElement file = parse(thrift); - StructElement union = file.unions().get(0); - AnnotationElement ann = union.annotations(); - - assertNotNull(ann); - assertThat(ann.get("layout"), is("padded")); - } - - @Test - public void annotationsOnExceptions() throws Exception { - String thrift = "" + - "exception Exception {\n" + - " 1: required i32 boom,\n" + - "} (java.runtime_exception)"; - - ThriftFileElement file = parse(thrift); - StructElement exception = file.exceptions().get(0); - AnnotationElement ann = exception.annotations(); - - assertNotNull(ann); - assertThat(ann.get("java.runtime_exception"), is("true")); - } - - @Test - public void annotationsOnFields() { - String thrift = "struct Str {\n" + - " 1: i32 what (what = 'what'),\n" + - " 2: binary data (compression = 'zlib') // doc\n" + - " 3: optional i8 bits (synonym = 'byte')\n" + - "}"; - - ThriftFileElement file = parse(thrift); - StructElement struct = file.structs().get(0); - - AnnotationElement anno = struct.fields().get(0).annotations(); - assertNotNull(anno); - assertThat(anno.get("what"), is("what")); - - anno = struct.fields().get(1).annotations(); - assertNotNull(anno); - assertThat(anno.get("compression"), is("zlib")); - - anno = struct.fields().get(2).annotations(); - assertNotNull(anno); - assertThat(anno.get("synonym"), is("byte")); - } - - @Test - public void annotationsOnFieldTypes() { - String thrift = "struct Str {\n" + - " 1: map (python.immutable) foo\n" + - "}"; - - ThriftFileElement file = parse(thrift); - StructElement struct = file.structs().get(0); - FieldElement field = struct.fields().get(0); - AnnotationElement anno = field.type().annotations(); - - assertNotNull(anno); - assertThat(anno.get("python.immutable"), is("true")); - } - - @Test - public void annotationsOnConsecutiveDefinitions() throws Exception { - String thrift = "" + - "namespace java com.foo.bar (ns = 'ok')\n" + - "enum Foo {} (enumAnno = 'yep')\n" + - ""; - - parse(thrift); - } - - @Test - public void annotationsWithEscapedQuotesInValues() throws Exception { - String thrift = "" + - "namespace java x (comment = \"what a \\\"mess\\\"\")\n"; - - ThriftFileElement fileElement = parse(thrift); - NamespaceElement ns = fileElement.namespaces().get(0); - assertThat(ns.annotations().get("comment"), is("what a \"mess\"")); - } - - @Test - public void newlinesAreTricky() { - // We must take care not to confuse the return type of the second - // function with a possible 'throws' clause from the not-definitively-finished - // first function. - String thrift = "" + - "typedef i32 typeof_int\n" + - "service Stupid {\n" + - " i32 foo()\n" + - " typeof_int bar()\n" + - "}"; - - parse(thrift); - } - - @Test - public void fieldsWithoutSeparatorsDoNotConsumeNextFieldsDocumentation() { - String thrift = "struct SomeRequest {\n" + - " /** Here's a comment. */\n" + - " 1: required UUID clientUuid\n" + - "\n" + - " /** Here's a longer comment. */\n" + - " 2: optional string someOtherField\n" + - "}"; - - ThriftFileElement element = parse(thrift); - StructElement struct = element.structs().get(0); - FieldElement clientUuid = struct.fields().get(0); - FieldElement someOtherField = struct.fields().get(1); - - assertThat(clientUuid.documentation(), is("Here's a comment.\n")); - assertThat(someOtherField.documentation(), is("Here's a longer comment.\n")); - } - - @Test - public void trailingDocWithoutSeparatorWithAnnotationOnNewLine() { - String thrift = "struct SomeRequest {\n" + - " /** Here's a comment. */\n" + - " 1: required UUID clientUuid\n" + - " (bork = \"bork\") // this belongs to clientUuid\n" + - "\n" + - " /**\n" + - " * Here's a longer comment.\n" + - " * One two lines.\n" + - " */\n" + - " 2: optional string someOtherField\n" + - "}"; - - ThriftFileElement element = parse(thrift); - StructElement struct = element.structs().get(0); - FieldElement clientUuid = struct.fields().get(0); - FieldElement someOtherField = struct.fields().get(1); - - assertThat(clientUuid.documentation(), containsString("this belongs to clientUuid")); - assertThat(someOtherField.documentation(), containsString("Here's a longer comment.")); - } - - @Test - public void enumJavadocWithoutSeparators() { - String thrift = "/**\n" + - " * Some Javadoc\n" + - " */\n" + - "enum Value {\n" + - " /**\n" + - " * This is not trailing doc.\n" + - " */\n" + - " FIRST\n" + - " /**\n" + - " * Neither is this.\n" + - " */\n" + - " SECOND\n" + - "}"; - - Location loc = Location.get("foo", "bar.thrift"); - EnumElement expected = EnumElement.builder(loc.at(4,1)) - .documentation("Some Javadoc\n") - .name("Value") - .members(ImmutableList.builder() - .add(EnumMemberElement.builder(loc.at(8, 5)) - .name("FIRST") - .value(0) - .documentation("This is not trailing doc.\n") - .build()) - .add(EnumMemberElement.builder(loc.at(12, 5)) - .name("SECOND") - .value(1) - .documentation("Neither is this.\n") - .build()) - .build()) - .build(); - - ThriftFileElement file = parse(thrift, loc); - - assertThat(file.enums().get(0), equalTo(expected)); - } - - @Test - public void structsCanOmitAndReorderFieldIds() { - ThriftFileElement element = parse("" + - "struct Struct {\n" + - " required string foo;\n" + - " required string bar;\n" + - " 5: required string baz\n" + - " required string qux;\n" + - " 4: required string barfo\n" + - " required string beefy\n" + - "}"); - - StructElement struct = element.structs().get(0); - ImmutableList fields = struct.fields(); - assertThat(fields.get(0).fieldId(), equalTo(1)); - assertThat(fields.get(1).fieldId(), equalTo(2)); - assertThat(fields.get(2).fieldId(), equalTo(5)); - assertThat(fields.get(3).fieldId(), equalTo(6)); - assertThat(fields.get(4).fieldId(), equalTo(4)); - assertThat(fields.get(5).fieldId(), equalTo(7)); - } - - @Test - public void commentsThatAreEmptyDoNotCrash() { - ThriftFileElement file = parse("" + - "//\n" + - "const i32 foo = 2"); - - ConstElement element = file.constants().get(0); - assertThat(element.documentation(), isEmptyString()); - } - - private static ThriftFileElement parse(String thrift) { - return parse(thrift, Location.get("", "")); - } - - private static ThriftFileElement parse(String thrift, Location location) { - return ThriftParser.parse(location, thrift, new ErrorReporter()); - } - -} diff --git a/thrifty-schema/src/test/kotlin/com/microsoft/thrifty/schema/parser/ThriftParserTest.kt b/thrifty-schema/src/test/kotlin/com/microsoft/thrifty/schema/parser/ThriftParserTest.kt new file mode 100644 index 0000000..eb3392a --- /dev/null +++ b/thrifty-schema/src/test/kotlin/com/microsoft/thrifty/schema/parser/ThriftParserTest.kt @@ -0,0 +1,1236 @@ +/* + * Thrifty + * + * Copyright (c) Microsoft Corporation + * + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING + * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, + * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. + * + * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. + */ +package com.microsoft.thrifty.schema.parser + +import com.microsoft.thrifty.schema.ErrorReporter +import com.microsoft.thrifty.schema.Location +import com.microsoft.thrifty.schema.NamespaceScope +import com.microsoft.thrifty.schema.Requiredness +import okio.Okio + +import org.junit.After +import org.junit.Before +import org.junit.Test + +import java.util.UUID + +import org.hamcrest.Matchers.containsString +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.isEmptyString +import org.junit.Assert.* + +class ThriftParserTest { + + @Before + fun setup() { + ThriftyParserPlugins.setUUIDProvider(object : ThriftyParserPlugins.UUIDProvider { + override fun call(): UUID { + return TEST_UUID + } + }) + } + + @After + fun tearDown() { + ThriftyParserPlugins.reset() + } + + @Test + fun namespaces() { + val thrift = "namespace java com.microsoft.thrifty.parser\n" + + "namespace cpp microsoft.thrifty\n" + + "namespace * microsoft.thrifty\n" + + "php_namespace 'single_quoted_namespace'\n" + + "php_namespace \"double_quoted_namespace\"" + + val location = Location.get("", "namespaces.thrift") + val file = parse(thrift, location) + + val expected = ThriftFileElement( + location = location, + namespaces = listOf( + NamespaceElement( + location = location.at(1, 1), + scope = NamespaceScope.JAVA, + namespace = "com.microsoft.thrifty.parser" + ), + NamespaceElement( + location = location.at(2, 1), + scope = NamespaceScope.CPP, + namespace = "microsoft.thrifty" + ), + NamespaceElement( + location = location.at(3, 1), + scope = NamespaceScope.ALL, + namespace = "microsoft.thrifty" + ), + NamespaceElement( + location = location.at(4, 1), + scope = NamespaceScope.PHP, + namespace = "single_quoted_namespace" + ), + NamespaceElement( + location = location.at(5, 1), + scope = NamespaceScope.PHP, + namespace = "double_quoted_namespace" + ) + ) + ) + + assertThat(file, equalTo(expected)) + } + + @Test + fun includes() { + val thrift = "include 'inc/common.thrift'\n" + + "include \".././parent.thrift\"\n" + + "cpp_include 'inc/boost.hpp'\n" + + "\n" + + "namespace * microsoft" + + val location = Location.get("", "includes.thrift") + val expected = ThriftFileElement( + location = location, + includes = listOf( + IncludeElement(location.at(1, 1), false, "inc/common.thrift"), + IncludeElement(location.at(2, 1), false, ".././parent.thrift"), + IncludeElement(location.at(3, 1), true, "inc/boost.hpp") + ), + namespaces = listOf( + NamespaceElement(location.at(5, 1), NamespaceScope.ALL, "microsoft") + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun simpleTypedefs() { + val thrift = "typedef i32 MyInt\n" + + "typedef string MyString\n" + + "typedef binary PrivateKey\n" + + val location = Location.get("", "typedefs.thrift") + + val expected = ThriftFileElement( + location = location, + typedefs = listOf( + TypedefElement( + location = location.at(1, 1), + oldType = TypeElement.scalar(location.at(1, 9), "i32", null), + newName = "MyInt"), + TypedefElement( + location = location.at(2, 1), + oldType = TypeElement.scalar(location.at(2, 9), "string", null), + newName = "MyString"), + TypedefElement( + location = location.at(3, 1), + oldType = TypeElement.scalar(location.at(3, 9), "binary", null), + newName = "PrivateKey") + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun containerTypedefs() { + val thrift = "typedef list IntList\n" + + "typedef set Names\n" + + "typedef map < i16,set > BlobMap\n" + + val location = Location.get("", "containerTypedefs.thrift") + + val expected = ThriftFileElement( + location = location, + typedefs = listOf( + TypedefElement( + location = location.at(1, 1), + oldType = TypeElement.list( + location.at(1, 9), + TypeElement.scalar(location.at(1, 14), "i32")), + newName = "IntList" + ), + TypedefElement( + location = location.at(2, 1), + oldType = TypeElement.set( + location.at(2, 9), + TypeElement.scalar(location.at(2, 13), "string")), + newName = "Names" + ), + TypedefElement( + location = location.at(3, 1), + oldType = TypeElement.map( + location.at(3, 9), + TypeElement.scalar(location.at(3, 15), "i16"), + TypeElement.set( + location.at(3, 19), + TypeElement.scalar(location.at(3, 23), "binary"))), + newName = "BlobMap" + ) + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun emptyStruct() { + val thrift = "struct Empty {}" + val location = Location.get("", "empty.thrift") + + val expected = ThriftFileElement( + location = location, + structs = listOf( + StructElement( + location = location.at(1, 1), + name = "Empty", + type = StructElement.Type.STRUCT, + fields = emptyList() + ) + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun simpleStruct() { + val thrift = "struct Simple {\n" + + " /** This field is optional */\n" + + " 1:i32 foo,\n" + + " // This next field is required\n" + + " 2: required string bar // and has trailing doc\n" + + "}" + + val location = Location.get("", "simple.thrift") + + val expected = ThriftFileElement( + location = location, + structs = listOf( + StructElement( + location = location.at(1, 1), + name = "Simple", + type = StructElement.Type.STRUCT, + fields = listOf( + FieldElement( + location = location.at(3, 3), + fieldId = 1, + type = TypeElement.scalar(location.at(3, 5), "i32"), + name = "foo", + requiredness = Requiredness.DEFAULT, + documentation = "This field is optional\n"), + FieldElement( + location = location.at(5, 3), + fieldId = 2, + type = TypeElement.scalar(location.at(5, 15), "string"), + name = "bar", + requiredness = Requiredness.REQUIRED, + documentation = "This next field is required\nand has trailing doc\n") + ) + ) + + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun trailingFieldDoc() { + val thrift = "" + + "// This struct demonstrates trailing comments\n" + + "struct TrailingDoc {\n" + + " 1: required string standard, // cpp-style\n" + + " 2: required string python, # py-style\n" + + " 3: optional binary knr; /** K&R-style **/\n" + + "}" + + val location = Location.get("", "trailing.thrift") + val expected = ThriftFileElement( + location = location, + structs = listOf( + StructElement( + location = location.at(2, 1), + name = "TrailingDoc", + type = StructElement.Type.STRUCT, + fields = listOf( + FieldElement( + location = location.at(3, 3), + fieldId = 1, + type = TypeElement.scalar(location.at(3, 15), "string"), + name = "standard", + requiredness = Requiredness.REQUIRED, + documentation = "cpp-style\n"), + FieldElement( + location = location.at(4, 3), + fieldId = 2, + type = TypeElement.scalar(location.at(4, 15), "string"), + name = "python", + requiredness = Requiredness.REQUIRED, + documentation = "py-style\n"), + FieldElement( + location = location.at(5, 3), + fieldId = 3, + type = TypeElement.scalar(location.at(5, 15), "binary"), + name = "knr", + requiredness = Requiredness.OPTIONAL, + documentation = "K&R-style *\n")), + documentation = "This struct demonstrates trailing comments\n") + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun duplicateFieldIds() { + val thrift = "struct Nope {\n" + + "1: string foo;\n" + + "1: string bar;\n" + + "}" + + try { + parse(thrift, Location.get("", "duplicateIds.thrift")) + fail("Structs with duplicate field IDs should fail to parse") + } catch (e: IllegalStateException) { + assertThat(e.message, containsString("duplicate field ID:")) + } + + } + + @Test + fun implicitDuplicateFieldIds() { + val thrift = "struct StillNope {\n" + + "string foo;\n" + + "string bar;\n" + + "1: bytes baz\n" + + "}" + + try { + parse(thrift, Location.get("", "duplicateImplicitIds.thrift")) + fail("Structs with duplicate implicit field IDs should fail to parse") + } catch (e: IllegalStateException) { + assertThat(e.message, containsString("duplicate field ID: 1")) + } + + } + + @Test + fun weirdFieldPermutations() { + val thrift = "struct WeirdButLegal {\n" + + "byte minimal\n" + + "byte minimalWithSeparator,\n" + + "byte minimalWithOtherSeparator;\n" + + "required byte requiredWithoutSeparator\n" + + "required byte requiredWithComma,\n" + + "required byte requiredWithSemicolon;\n" + + "optional i16 optionalWithoutSeparator\n" + + "optional i16 optionalWithComma,\n" + + "optional i16 optionalWithSemicolon;\n" + + "10: i32 implicitOptional\n" + + "11: i32 implicitOptionalWithComma,\n" + + "12: i32 implicitOptionalWithSemicolon;\n" + + "13: required i64 requiredId\n" + + "14: required i64 requiredIdWithComma,\n" + + "15: required i64 requiredIdWithSemicolon;\n" + + "}" + + val location = Location.get("", "weird.thrift") + + val expectedStruct = StructElement( + location = location.at(1, 1), + name = "WeirdButLegal", + type = StructElement.Type.STRUCT, + fields = listOf( + FieldElement( + location = location.at(2, 1), + fieldId = 1, + type = TypeElement.scalar(location.at(2, 1), "byte", null), + name = "minimal", + requiredness = Requiredness.DEFAULT), + FieldElement( + location = location.at(3, 1), + fieldId = 2, + type = TypeElement.scalar(location.at(3, 1), "byte", null), + name = "minimalWithSeparator", + requiredness = Requiredness.DEFAULT), + FieldElement( + location = location.at(4, 1), + fieldId = 3, + type = TypeElement.scalar(location.at(4, 1), "byte", null), + name = "minimalWithOtherSeparator", + requiredness = Requiredness.DEFAULT), + FieldElement( + location = location.at(5, 1), + fieldId = 4, + type = TypeElement.scalar(location.at(5, 10), "byte", null), + name = "requiredWithoutSeparator", + requiredness = Requiredness.REQUIRED), + FieldElement( + location = location.at(6, 1), + fieldId = 5, + type = TypeElement.scalar(location.at(6, 10), "byte", null), + name = "requiredWithComma", + requiredness = Requiredness.REQUIRED), + FieldElement( + location = location.at(7, 1), + fieldId = 6, + type = TypeElement.scalar(location.at(7, 10), "byte", null), + name = "requiredWithSemicolon", + requiredness = Requiredness.REQUIRED), + FieldElement( + location = location.at(8, 1), + fieldId = 7, + type = TypeElement.scalar(location.at(8, 10), "i16", null), + name = "optionalWithoutSeparator", + requiredness = Requiredness.OPTIONAL), + FieldElement( + location = location.at(9, 1), + fieldId = 8, + type = TypeElement.scalar(location.at(9, 10), "i16", null), + name = "optionalWithComma", + requiredness = Requiredness.OPTIONAL), + FieldElement( + location = location.at(10, 1), + fieldId = 9, + type = TypeElement.scalar(location.at(10, 10), "i16", null), + name = "optionalWithSemicolon", + requiredness = Requiredness.OPTIONAL), + FieldElement( + location = location.at(11, 1), + fieldId = 10, + type = TypeElement.scalar(location.at(11, 5), "i32", null), + name = "implicitOptional", + requiredness = Requiredness.DEFAULT), + FieldElement( + location = location.at(12, 1), + fieldId = 11, + type = TypeElement.scalar(location.at(12, 5), "i32", null), + name = "implicitOptionalWithComma", + requiredness = Requiredness.DEFAULT), + FieldElement( + location = location.at(13, 1), + fieldId = 12, + type = TypeElement.scalar(location.at(13, 5), "i32", null), + name = "implicitOptionalWithSemicolon", + requiredness = Requiredness.DEFAULT), + FieldElement( + location = location.at(14, 1), + fieldId = 13, + type = TypeElement.scalar(location.at(14, 14), "i64", null), + name = "requiredId", + requiredness = Requiredness.REQUIRED), + FieldElement( + location = location.at(15, 1), + fieldId = 14, + type = TypeElement.scalar(location.at(15, 14), "i64", null), + name = "requiredIdWithComma", + requiredness = Requiredness.REQUIRED), + FieldElement( + location = location.at(16, 1), + fieldId = 15, + type = TypeElement.scalar(location.at(16, 14), "i64", null), + name = "requiredIdWithSemicolon", + requiredness = Requiredness.REQUIRED) + ) + ) + + val expected = ThriftFileElement( + location = location, + structs = listOf(expectedStruct) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun invalidFieldIds() { + val negativeIdThrift = "struct NegativeId { -1: required i32 nope }" + try { + parse(negativeIdThrift) + fail("Should not parse a struct with a negative field ID") + } catch (e: IllegalStateException) { + assertThat(e.message, containsString("field ID must be greater than zero")) + } + + val zeroIdThrift = "struct ZeroId {\n" + + " 0: optional i64 stillNope\n" + + "}" + try { + parse(zeroIdThrift) + fail("Should not parse a struct with a zero field ID") + } catch (e: IllegalStateException) { + assertThat(e.message, containsString("field ID must be greater than zero")) + } + + } + + @Test + fun services() { + // NOTE: the service defined below is *not a legal Thrift service*. + // 'oneway' functions must return void, and may not throw. + // We don't do this level of semantic validation here. + val thrift = "" + + "service Svc {\n" + + " FooResult foo(1:FooRequest request, 2: optional FooMeta meta)\n" + + " oneway BarResult bar() throws (1:FooException foo, 2:BarException bar)\n" + + "}" + + val location = Location.get("", "simpleService.thrift") + val expected = ThriftFileElement( + location = location, + services = listOf( + ServiceElement( + location.at(1, 1), + "Svc", + listOf( + FunctionElement( + location = location.at(2, 3), + name = "foo", + returnType = TypeElement.scalar(location.at(2, 3), "FooResult"), + params = listOf( + FieldElement(location.at(2, 17), + 1, + TypeElement.scalar(location.at(2, 19), "FooRequest"), + "request", + Requiredness.REQUIRED), + FieldElement( + location.at(2, 39), + 2, + TypeElement.scalar(location.at(2, 51), "FooMeta"), + "meta", + Requiredness.OPTIONAL) + ) + ), + FunctionElement( + location = location.at(3, 3), + name = "bar", + returnType = TypeElement.scalar(location.at(3, 10), "BarResult"), + exceptions = listOf( + FieldElement(location.at(3, 34), + 1, + TypeElement.scalar(location.at(3, 36), "FooException"), + "foo", + Requiredness.DEFAULT), + FieldElement(location.at(3, 54), + 2, + TypeElement.scalar(location.at(3, 56), "BarException"), + "bar", + Requiredness.DEFAULT) + ), + oneWay = true + ) + ) + ) + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun serviceWithNewlineBeforeThrows() { + val thrift = "" + + "service Svc {\n" + + " void foo()\n" + + " throws (1: Blargh blah)\n" + + " i32 bar()\n" + + "}" + + val location = Location.get("", "services.thrift") + val expected = ThriftFileElement( + location = location, + services = listOf( + ServiceElement( + location = location.at(1, 1), + name = "Svc", + functions = listOf( + FunctionElement( + location = location.at(2, 3), + name = "foo", + returnType = TypeElement.scalar(location.at(2, 3), "void"), + exceptions = listOf(FieldElement( + location = location.at(3, 13), + fieldId = 1, + type = TypeElement.scalar(location.at(3, 16), "Blargh"), + name = "blah", + requiredness = Requiredness.DEFAULT))), + FunctionElement( + location = location.at(4, 3), + name = "bar", + returnType = TypeElement.scalar(location.at(4, 3), "i32")) + )))) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun unions() { + val thrift = "" + + "union Normal {\n" + + " 2: i16 foo,\n" + + " 4: i32 bar\n" + + "}\n" + + val location = Location.get("", "union.thrift") + + val expected = ThriftFileElement(location, + unions = listOf( + StructElement( + location = location.at(1, 1), + name = "Normal", + type = StructElement.Type.UNION, + fields = listOf( + FieldElement(location.at(2, 3), + 2, + TypeElement.scalar(location.at(2, 6), "i16"), + "foo", + Requiredness.DEFAULT), + FieldElement(location.at(3, 3), + 4, + TypeElement.scalar(location.at(3, 6), "i32"), + "bar", + Requiredness.DEFAULT) + ) + ) + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun unionCannotHaveRequiredField() { + val thrift = "\n" + + "union Normal {\n" + + " 3: optional i16 foo,\n" + + " 5: required i32 bar\n" + + "}\n" + + try { + parse(thrift, Location.get("", "unionWithRequired.thrift")) + fail("Union cannot have a required field") + } catch (e: IllegalStateException) { + assertThat(e.message, containsString("unions cannot have required fields")) + } + + } + + @Test + fun unionCannotHaveMultipleDefaultValues() { + val thrift = "\n" + + "union Normal {\n" + + " 3: i16 foo = 1,\n" + + " 5: i32 bar = 2\n" + + "}\n" + + try { + parse(thrift, Location.get("", "unionWithRequired.thrift")) + fail("Union cannot have a more than one default value") + } catch (e: IllegalStateException) { + assertThat(e.message, containsString("unions can have at most one default value")) + } + + } + + @Test + fun unionsCanHaveOneDefaultValue() { + val thrift = "" + + "union Default {\n" + + " 1: i16 foo,\n" + + " 2: i16 bar,\n" + + " 3: i16 baz = 0x0FFF,\n" + + " 4: i16 quux\n" + + "}" + + val location = Location.get("", "unionWithDefault.thrift") + + val expected = ThriftFileElement( + location = location, + unions = listOf( + StructElement( + location = location.at(1, 1), + name = "Default", + type = StructElement.Type.UNION, + fields = listOf( + FieldElement( + location = location.at(2, 3), + fieldId = 1, + type = TypeElement.scalar(location.at(2, 6), "i16"), + name = "foo", + requiredness = Requiredness.DEFAULT), + FieldElement( + location = location.at(3, 3), + fieldId = 2, + type = TypeElement.scalar(location.at(3, 6), "i16"), + name = "bar", + requiredness = Requiredness.DEFAULT), + FieldElement( + location = location.at(4, 3), + fieldId = 3, + type = TypeElement.scalar(location.at(4, 6), "i16"), + name = "baz", + requiredness = Requiredness.DEFAULT, + constValue = ConstValueElement.integer( + location.at(4, 16), + "0x0FFF", + 0xFFF)), + FieldElement( + location = location.at(5, 3), + fieldId = 4, + type = TypeElement.scalar(location.at(5, 6), "i16"), + name = "quux", + requiredness = Requiredness.DEFAULT) + ) + ) + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun simpleConst() { + val thrift = "const i64 DefaultStatusCode = 200" + val location = Location.get("", "simpleConst.thrift") + + val expected = ThriftFileElement( + location = location, + constants = listOf( + ConstElement( + location = location.at(1, 1), + type = TypeElement.scalar(location.at(1, 7), "i64"), + name = "DefaultStatusCode", + value = ConstValueElement.integer(location.at(1, 31), "200", 200) + ) + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun veryLargeConst() { + val thrift = "const i64 Yuuuuuge = 0xFFFFFFFFFF" + val location = Location.get("", "veryLargeConst.thrift") + + val expected = ThriftFileElement( + location, + constants = listOf( + ConstElement( + location = location.at(1, 1), + type = TypeElement.scalar(location.at(1, 7), "i64", null), + name = "Yuuuuuge", + value = ConstValueElement.integer( + location = location.at(1, 22), + text = "0xFFFFFFFFFF", + value = 0xFFFFFFFFFFL + ) + ) + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun listConst() { + val thrift = "const list Names = [\"foo\" \"bar\", \"baz\"; \"quux\"]" + val location = Location.get("", "listConst.thrift") + + val element = ConstElement( + location.at(1, 1), + type = TypeElement.list( + location = location.at(1, 7), + elementType = TypeElement.scalar(location.at(1, 12), "string")), + name = "Names", + value = ConstValueElement.list( + location = location.at(1, 28), + text = "[\"foo\"\"bar\",\"baz\";\"quux\"]", + value = listOf( + ConstValueElement.literal(location.at(1, 29), "\"foo\"", "foo"), + ConstValueElement.literal(location.at(1, 35), "\"bar\"", "bar"), + ConstValueElement.literal(location.at(1, 42), "\"baz\"", "baz"), + ConstValueElement.literal(location.at(1, 49), "\"quux\"", "quux")))) + + val expected = ThriftFileElement( + location = location, + constants = listOf(element) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun mapConst() { + val thrift = "const map Headers = {\n" + + " \"foo\": \"bar\",\n" + + " \"baz\": \"quux\";\n" + + "}" + + val location = Location.get("", "mapConst.thrift") + val mapConst = ConstElement( + location = location.at(1, 1), + type = TypeElement.map( + location = location.at(1, 7), + keyType = TypeElement.scalar(location.at(1, 11), "string", null), + valueType = TypeElement.scalar(location.at(1, 19), "string", null)), + name = "Headers", + value = ConstValueElement.map( + location = location.at(1, 37), + text = "{\"foo\":\"bar\",\"baz\":\"quux\";}", + value = mapOf( + ConstValueElement.literal(location.at(2, 3), "\"foo\"", "foo") to + ConstValueElement.literal(location.at(2, 10), "\"bar\"", "bar"), + + ConstValueElement.literal(location.at(3, 3), "\"baz\"", "baz") to + ConstValueElement.literal(location.at(3, 10), "\"quux\"", "quux")))) + + val expected = ThriftFileElement( + location = location, + constants = listOf(mapConst) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun structFieldWithConstValue() { + val thrift = "struct Foo {\n" + + " 100: i32 num = 1\n" + + "}" + + val location = Location.get("", "structWithConstValue.thrift") + + val expected = ThriftFileElement( + location = location, + structs = listOf( + StructElement( + location = location.at(1, 1), + name = "Foo", + type = StructElement.Type.STRUCT, + fields = listOf( + FieldElement( + location = location.at(2, 3), + fieldId = 100, + type = TypeElement.scalar(location.at(2, 8), "i32"), + name = "num", + constValue = ConstValueElement.integer(location.at(2, 18), "1", 1) + ) + ) + ) + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun canParseOfficialTestCase() { + val classLoader = javaClass.classLoader + val stream = classLoader.getResourceAsStream("cases/TestThrift.thrift") + val thrift = Okio.buffer(Okio.source(stream)).readUtf8() + val location = Location.get("cases", "TestThrift.thrift") + + // Not crashing is good enough here. We'll be more strict with this file in the loader test. + parse(thrift, location) + } + + @Test + fun bareEnums() { + val thrift = "enum Enum {\n" + + " FOO,\n" + + " BAR\n" + + "}" + + val location = Location.get("", "bareEnums.thrift") + val expected = ThriftFileElement( + location = location, + enums = listOf( + EnumElement( + location = location.at(1, 1), + name = "Enum", + members = listOf( + EnumMemberElement(location.at(2, 3), "FOO", 0), + EnumMemberElement(location.at(3, 3), "BAR", 1)) + ) + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun enumWithLargeGaps() { + val thrift = "enum Gaps {\n" + + " SMALL = 10,\n" + + " MEDIUM = 100,\n" + + " ALSO_MEDIUM,\n" + + " LARGE = 5000\n" + + "}" + + val location = Location.get("", "enumWithLargeGaps.thrift") + val expected = ThriftFileElement( + location = location, + enums = listOf( + EnumElement( + location = location.at(1, 1), + name = "Gaps", + members = listOf( + EnumMemberElement(location.at(2, 3), "SMALL", 10), + EnumMemberElement(location.at(3, 3), "MEDIUM", 100), + EnumMemberElement(location.at(4, 3), "ALSO_MEDIUM", 101), + EnumMemberElement(location.at(5, 3), "LARGE", 5000) + ) + ) + ) + ) + + assertThat(parse(thrift, location), equalTo(expected)) + } + + @Test + fun defaultValuesCanClash() { + val thrift = "enum Enum {\n" + + " FOO = 5,\n" + + " BAR = 4,\n" + + " BAZ\n" + + "}" + + try { + parse(thrift) + fail() + } catch (e: IllegalStateException) { + assertThat(e.message, containsString("duplicate enum value")) + } + + } + + @Test + fun annotationsOnNamespaces() { + val thrift = "namespace java com.test (foo = 'bar')" + val ns = parse(thrift).namespaces.single() + val ann = ns.annotations!! + + assertThat(ann["foo"], equalTo("bar")) + } + + @Test + fun annotationsOnTypedefs() { + val thrift = "namespace java com.test\n" + + "\n" + + "typedef i32 StatusCode (boxed = 'false');" + + val typedef = parse(thrift).typedefs.single() + val ann = typedef.annotations!! + + assertThat(ann["boxed"], equalTo("false")) + } + + @Test + @Throws(Exception::class) + fun annotationsOnEnums() { + val thrift = "enum Foo {} (bar = 'baz')" + val enum = parse(thrift).enums.single() + val ann = enum.annotations!! + + assertThat(ann["bar"], equalTo("baz")) + } + + @Test + fun annotationsOnEnumMembers() { + val thrift = "" + + "enum Foo {\n" + + " BAR (bar = 'abar'),\n" + + " BAZ = 1 (baz = 'abaz'),\n" + + " QUX (qux = 'aqux')\n" + + " WOO\n" + + "}" + + val enum = parse(thrift).enums.single() + val (a1, a2, a3, a4) = enum.members.map { it.annotations } + + + assertThat(a1!!["bar"], equalTo("abar")) + assertThat(a2!!["baz"], equalTo("abaz")) + assertThat(a3!!["qux"], equalTo("aqux")) + assertNull(a4) + } + + @Test + fun annotationsOnServices() { + val thrift = "" + + "service Svc {" + + " void foo(1: i32 bar)" + + "} (async = 'true', java.races = 'false')" + val services = parse(thrift).services + val ann = services.single().annotations!! + + assertThat(ann.size, equalTo(2)) + assertThat(ann["async"], equalTo("true")) + assertThat(ann["java.races"], equalTo("false")) + } + + @Test + fun annotationsOnFunctions() { + val thrift = "service Svc {\n" + + " void nothrow() (test = 'a'),\n" + + " void nosep() (test = 'b')\n" + + " i32 hasThrow() throws(1: string what) (test = 'c');\n" + + "}" + + val services = parse(thrift).services + val functions = services[0].functions + + val (a, b, c) = functions.map { it.annotations} + + assertNotNull(a) + assertNotNull(b) + assertNotNull(c) + + assertThat(a?.get("test"), equalTo("a")) + assertThat(b?.get("test"), equalTo("b")) + assertThat(c?.get("test"), equalTo("c")) + } + + @Test + fun annotationsOnStructs() { + val thrift = "" + + "struct Str {\n" + + " 1: i32 hi,\n" + + " 2: optional string there,\n" + + "} (layout = 'sequential')" + + val structs = parse(thrift).structs + val ann = structs[0].annotations + + assertNotNull(ann) + assertThat(ann?.get("layout"), equalTo("sequential")) + } + + @Test + fun annotationsOnUnions() { + val thrift = "" + + "union Union {\n" + + " 1: i32 hi,\n" + + " 2: optional string there,\n" + + "} (layout = 'padded')" + + val unions = parse(thrift).unions + val ann = unions[0].annotations + + assertNotNull(ann) + assertThat(ann?.get("layout"), equalTo("padded")) + } + + @Test + fun annotationsOnExceptions() { + val thrift = "" + + "exception Exception {\n" + + " 1: required i32 boom,\n" + + "} (java.runtime_exception)" + + val exceptions = parse(thrift).exceptions + val ann = exceptions[0].annotations + + assertNotNull(ann) + assertThat(ann?.get("java.runtime_exception"), equalTo("true")) + } + + @Test + fun annotationsOnFields() { + val thrift = "struct Str {\n" + + " 1: i32 what (what = 'what'),\n" + + " 2: binary data (compression = 'zlib') // doc\n" + + " 3: optional i8 bits (synonym = 'byte')\n" + + "}" + + val structs = parse(thrift).structs + val fields = structs[0].fields + + assertThat(fields[0].annotations?.get("what"), equalTo("what")) + + assertThat(fields[1].annotations?.get("compression"), equalTo("zlib")) + + assertThat(fields[2].annotations?.get("synonym"), equalTo("byte")) + } + + @Test + fun annotationsOnFieldTypes() { + val thrift = "struct Str {\n" + + " 1: map (python.immutable) foo\n" + + "}" + + val structs = parse(thrift).structs + val fields = structs[0].fields + val anno = fields[0].type.annotations + + assertNotNull(anno) + assertThat(anno?.get("python.immutable"), equalTo("true")) + } + + @Test + fun annotationsOnConsecutiveDefinitions() { + val thrift = "" + + "namespace java com.foo.bar (ns = 'ok')\n" + + "enum Foo {} (enumAnno = 'yep')\n" + + "" + + parse(thrift) + } + + @Test + fun annotationsWithEscapedQuotesInValues() { + val thrift = "" + "namespace java x (comment = \"what a \\\"mess\\\"\")\n" + + val namespaces = parse(thrift).namespaces + val annotations = namespaces[0].annotations + assertThat(annotations?.get("comment"), equalTo("what a \"mess\"")) + } + + @Test + fun newlinesAreTricky() { + // We must take care not to confuse the return type of the second + // function with a possible 'throws' clause from the not-definitively-finished + // first function. + val thrift = "" + + "typedef i32 typeof_int\n" + + "service Stupid {\n" + + " i32 foo()\n" + + " typeof_int bar()\n" + + "}" + + parse(thrift) + } + + @Test + fun fieldsWithoutSeparatorsDoNotConsumeNextFieldsDocumentation() { + val thrift = "struct SomeRequest {\n" + + " /** Here's a comment. */\n" + + " 1: required UUID clientUuid\n" + + "\n" + + " /** Here's a longer comment. */\n" + + " 2: optional string someOtherField\n" + + "}" + + val structs = parse(thrift).structs + val fields = structs[0].fields + + assertThat(fields[0].documentation, equalTo("Here's a comment.\n")) + assertThat(fields[1].documentation, equalTo("Here's a longer comment.\n")) + } + + @Test + fun trailingDocWithoutSeparatorWithAnnotationOnNewLine() { + val thrift = "struct SomeRequest {\n" + + " /** Here's a comment. */\n" + + " 1: required UUID clientUuid\n" + + " (bork = \"bork\") // this belongs to clientUuid\n" + + "\n" + + " /**\n" + + " * Here's a longer comment.\n" + + " * One two lines.\n" + + " */\n" + + " 2: optional string someOtherField\n" + + "}" + + val structs = parse(thrift).structs + val fields = structs[0].fields + + assertThat(fields[0].documentation, containsString("this belongs to clientUuid")) + assertThat(fields[1].documentation, containsString("Here's a longer comment.")) + } + + @Test + fun enumJavadocWithoutSeparators() { + val thrift = "/**\n" + + " * Some Javadoc\n" + + " */\n" + + "enum Value {\n" + + " /**\n" + + " * This is not trailing doc.\n" + + " */\n" + + " FIRST\n" + + " /**\n" + + " * Neither is this.\n" + + " */\n" + + " SECOND\n" + + "}" + + val loc = Location.get("foo", "bar.thrift") + val expected = EnumElement( + location = loc.at(4, 1), + name = "Value", + members = listOf( + EnumMemberElement(loc.at(8, 5), "FIRST", 0, "This is not trailing doc.\n"), + EnumMemberElement(loc.at(12, 5), "SECOND", 1, "Neither is this.\n") + ), + documentation = "Some Javadoc\n") + + val enums = parse(thrift, loc).enums + + assertThat(enums[0], equalTo(expected)) + } + + @Test + fun structsCanOmitAndReorderFieldIds() { + val file = parse("" + + "struct Struct {\n" + + " required string foo;\n" + + " required string bar;\n" + + " 5: required string baz\n" + + " required string qux;\n" + + " 4: required string barfo\n" + + " required string beefy\n" + + "}") + + val structs = file.structs + val fields = structs[0].fields + + assertThat(fields[0].fieldId, equalTo(1)) + assertThat(fields[1].fieldId, equalTo(2)) + assertThat(fields[2].fieldId, equalTo(5)) + assertThat(fields[3].fieldId, equalTo(6)) + assertThat(fields[4].fieldId, equalTo(4)) + assertThat(fields[5].fieldId, equalTo(7)) + } + + @Test + fun commentsThatAreEmptyDoNotCrash() { + val file = parse("" + + "//\n" + + "const i32 foo = 2") + val constants = file.constants + val documentation = constants[0].documentation + assertThat(documentation, isEmptyString()) + } + + companion object { + + private val TEST_UUID = UUID.fromString("ecafa042-668a-4403-a6d3-70983866ffbe") + + private fun parse(thrift: String, location: Location = Location.get("", "")): ThriftFileElement { + return ThriftParser.parse(location, thrift, ErrorReporter()) + } + } + +}