ProgramLoader.java

/*
 * Copyright 2016 Providence Authors
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package net.morimekta.providence.reflect;

import net.morimekta.providence.PMessage;
import net.morimekta.providence.PMessageVariant;
import net.morimekta.providence.PType;
import net.morimekta.providence.descriptor.PAnnotation;
import net.morimekta.providence.descriptor.PContainer;
import net.morimekta.providence.descriptor.PDeclaredDescriptor;
import net.morimekta.providence.descriptor.PDescriptor;
import net.morimekta.providence.descriptor.PDescriptorProvider;
import net.morimekta.providence.descriptor.PEnumDescriptor;
import net.morimekta.providence.descriptor.PField;
import net.morimekta.providence.descriptor.PInterfaceDescriptorProvider;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PPrimitive;
import net.morimekta.providence.descriptor.PRequirement;
import net.morimekta.providence.descriptor.PService;
import net.morimekta.providence.descriptor.PServiceMethod;
import net.morimekta.providence.descriptor.PServiceProvider;
import net.morimekta.providence.descriptor.PStructDescriptor;
import net.morimekta.providence.descriptor.PStructDescriptorProvider;
import net.morimekta.providence.reflect.contained.CConst;
import net.morimekta.providence.reflect.contained.CEnumDescriptor;
import net.morimekta.providence.reflect.contained.CEnumValue;
import net.morimekta.providence.reflect.contained.CException;
import net.morimekta.providence.reflect.contained.CExceptionDescriptor;
import net.morimekta.providence.reflect.contained.CField;
import net.morimekta.providence.reflect.contained.CInterface;
import net.morimekta.providence.reflect.contained.CInterfaceDescriptor;
import net.morimekta.providence.reflect.contained.CProgram;
import net.morimekta.providence.reflect.contained.CService;
import net.morimekta.providence.reflect.contained.CServiceMethod;
import net.morimekta.providence.reflect.contained.CStruct;
import net.morimekta.providence.reflect.contained.CStructDescriptor;
import net.morimekta.providence.reflect.contained.CUnion;
import net.morimekta.providence.reflect.contained.CUnionDescriptor;
import net.morimekta.providence.reflect.model.AnnotationDeclaration;
import net.morimekta.providence.reflect.model.ConstDeclaration;
import net.morimekta.providence.reflect.model.Declaration;
import net.morimekta.providence.reflect.model.EnumDeclaration;
import net.morimekta.providence.reflect.model.EnumValueDeclaration;
import net.morimekta.providence.reflect.model.FieldDeclaration;
import net.morimekta.providence.reflect.model.IncludeDeclaration;
import net.morimekta.providence.reflect.model.MessageDeclaration;
import net.morimekta.providence.reflect.model.MethodDeclaration;
import net.morimekta.providence.reflect.model.NamespaceDeclaration;
import net.morimekta.providence.reflect.model.ProgramDeclaration;
import net.morimekta.providence.reflect.model.ServiceDeclaration;
import net.morimekta.providence.reflect.model.TypedefDeclaration;
import net.morimekta.providence.reflect.parser.ThriftException;
import net.morimekta.providence.reflect.parser.ThriftParser;
import net.morimekta.providence.reflect.parser.ThriftToken;
import net.morimekta.providence.reflect.util.ConstValueProvider;
import net.morimekta.providence.types.TypeReference;
import net.morimekta.providence.types.TypeRegistry;
import net.morimekta.util.Strings;
import net.morimekta.util.collect.UnmodifiableList;
import net.morimekta.util.collect.UnmodifiableMap;
import net.morimekta.util.lexer.UncheckedLexerException;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import static net.morimekta.providence.reflect.util.ReflectionUtils.isApacheThriftFile;
import static net.morimekta.providence.reflect.util.ReflectionUtils.isProvidenceFile;
import static net.morimekta.providence.reflect.util.ReflectionUtils.isThriftBasedFileSyntax;
import static net.morimekta.providence.reflect.util.ReflectionUtils.longestCommonPrefixPath;
import static net.morimekta.providence.reflect.util.ReflectionUtils.stripCommonPrefix;
import static net.morimekta.providence.types.TypeReference.parseType;
import static net.morimekta.providence.types.TypeReference.ref;
import static net.morimekta.util.FileUtil.readCanonicalPath;
import static net.morimekta.util.collect.UnmodifiableList.copyOf;
import static net.morimekta.util.collect.UnmodifiableList.listOf;

/**
 * Class that loads programs and packages them into a contained type structure.
 */
public class ProgramLoader {
    private final GlobalRegistry globalRegistry;

    private final ThriftParser thriftParser;
    private final ThriftParser providenceParser;

    /**
     * Constructor with lax and default behavior.
     */
    public ProgramLoader() {
        this(false,
             false,
             true);
    }

    /**
     * Constructor with specified options.
     *
     * @param requireFieldId If field IDs are required.
     * @param requireEnumValue If enum values are required.
     * @param allowLanguageReservedNames If language-reserved names are allowed.
     */
    public ProgramLoader(boolean requireFieldId,
                         boolean requireEnumValue,
                         boolean allowLanguageReservedNames) {
        this(new GlobalRegistry(),
             requireFieldId,
             requireEnumValue,
             allowLanguageReservedNames);
    }

    private ProgramLoader(@Nonnull GlobalRegistry registry,
                          boolean requireFieldId,
                          boolean requireEnumValue,
                          boolean allowLanguageReservedNames) {
        this.globalRegistry = registry;
        this.thriftParser = new ThriftParser(requireFieldId,
                                             requireEnumValue,
                                             allowLanguageReservedNames,
                                             false);
        this.providenceParser = new ThriftParser(requireFieldId,
                                                 requireEnumValue,
                                                 allowLanguageReservedNames,
                                                 true);
    }

    public ProgramRegistry load(File file) throws IOException {
        return load(file.toPath());
    }

    /**
     * Load a thrift definition from file including all it's dependencies.
     *
     * @param file The file to load.
     * @return The loaded contained document.
     * @throws IOException If the file could not be read or parsed.
     */
    public ProgramRegistry load(Path file) throws IOException {
        try {

            file = readCanonicalPath(file.toAbsolutePath().normalize());
            if (!Files.exists(file)) {
                throw new IllegalArgumentException("No such file " + file);
            }
            if (!Files.isRegularFile(file)) {
                throw new IllegalArgumentException(
                        "Unable to load thrift program: " + file + " is not a file.");
            }

            return loadInternal(file, listOf());
        } catch (ThriftException e) {
            if (e.getFile() == null) {
                e.setFile(file.getFileName().toString());
            }
            throw e;
        }
    }

    /**
     * Load internally.
     *
     * @param inFile The canonical normalized recolved path to parse.
     * @param loadStack The files loaded as part of getting here.
     * @return The loaded program.
     * @throws IOException If loading the program failed.
     */
    private ProgramRegistry loadInternal(Path inFile, List<Path> loadStack) throws IOException {
        loadStack = new ArrayList<>(loadStack);
        loadStack.add(inFile);

        ProgramRegistry registry = this.globalRegistry.registryForPath(inFile.toString());
        if (registry.getProgram() != null) {
            return registry;
        }

        ProgramDeclaration programType;
        try (InputStream in = new BufferedInputStream(Files.newInputStream(inFile))) {
            if (isThriftBasedFileSyntax(inFile)) {
                if (isProvidenceFile(inFile)) {
                    programType = providenceParser.parse(in, inFile);
                } else {
                    programType = thriftParser.parse(in, inFile);
                }
            } else {
                throw new IllegalArgumentException("Not a known providence source format: " + inFile);
            }
        }

        for (IncludeDeclaration include : programType.getIncludes()) {
            String includePath = include.getFilePath();
            Path location = inFile.getParent().resolve(includePath).normalize();
            if (!Files.exists(location)) {
                throw new ThriftException(
                        include.getFilePathToken(),
                        "No such file: " + location.toString());
            }
            Path canonicalPath = readCanonicalPath(location.toAbsolutePath().normalize());
            if (!Files.exists(canonicalPath)) {
                throw new ThriftException(
                        include.getFilePathToken(),
                        "No such file: " + canonicalPath.toString());
            }
            if (!Files.isRegularFile(canonicalPath)) {
                throw new ThriftException(
                        include.getFilePathToken(),
                        "Not a file: " + canonicalPath.toString());
            }
            if (loadStack.contains(canonicalPath)) {
                // circular includes.

                // Only show the circular includes, not the path to get there.
                while (!loadStack.get(0).equals(canonicalPath)) {
                    loadStack.remove(0);
                }
                loadStack.add(canonicalPath);
                List<String> loadStackAsString = loadStack.stream().map(Path::toString).collect(Collectors.toList());

                String prefix = longestCommonPrefixPath(loadStackAsString);
                if (prefix.length() > 0) {
                    loadStackAsString = stripCommonPrefix(loadStackAsString);
                }
                throw new ThriftException(
                        include.getFilePathToken(),
                        "Circular includes: " + String.join(" -> ", loadStackAsString));
            }

            if (isThriftBasedFileSyntax(canonicalPath)) {
                // Apache thrift cannot include providence files.
                if (isApacheThriftFile(inFile) &&
                    isProvidenceFile(canonicalPath)) {
                    throw new ThriftException(
                            include.getFilePathToken(),
                            "Not a thrift file: " + location.getFileName().toString());
                }
            } else {
                throw new ThriftException(
                        include.getFilePathToken(),
                        "Not a valid include format: " + location.getFileName().toString());
            }

            registry.addInclude(include.getProgramName(), loadInternal(location, copyOf(loadStack)));
        }

        // Now everything it depends on is loaded.

        CProgram program = convert(inFile, programType);
        registry.setProgramType(programType);
        registry.setProgram(program);
        return registry;
    }

    /**
     * @return The local registry.
     */
    public GlobalRegistry getGlobalRegistry() {
        return globalRegistry;
    }

    /**
     * Convert document model to declared document.
     *
     * @param path The program file path.
     * @param program Program model to convert.
     * @return The declared thrift document.
     * @throws ThriftException When validation or const parsing fails.
     */
    private CProgram convert(Path path, ProgramDeclaration program) throws ThriftException {
        UnmodifiableList.Builder<PDeclaredDescriptor<?>> declaredTypes = UnmodifiableList.builder();
        UnmodifiableList.Builder<CConst>                 constants     = UnmodifiableList.builder();
        UnmodifiableMap.Builder<String, String>          typedefs      = UnmodifiableMap.builder();
        UnmodifiableList.Builder<CService>               services      = UnmodifiableList.builder();

        ProgramRegistry registry = globalRegistry.registryForPath(path.toString());

        Path dir = path.getParent();
        for (IncludeDeclaration include : program.getIncludes()) {
            Path includePath = dir.resolve(include.getFilePath());
            try {
                includePath = readCanonicalPath(includePath);
            } catch (IOException e) {
                throw new ThriftException(include.getFilePathToken(),
                                          "Bad include path: %s", e.getMessage());
            }
            registry.addInclude(include.getProgramName(), globalRegistry.registryForPath(includePath.toString()));
        }
        String fileName = path.getFileName().toString();

        for (Declaration declaration : program.getDeclarationList()) {
            if (declaration instanceof EnumDeclaration) {
                registerEnum(program, (EnumDeclaration) declaration, declaredTypes, registry);
            } else if (declaration instanceof MessageDeclaration) {
                registerMessage(fileName, program, (MessageDeclaration) declaration, declaredTypes, registry);
            } else if (declaration instanceof ServiceDeclaration) {
                registerService(fileName, program, (ServiceDeclaration) declaration, services, registry);
            } else if (declaration instanceof TypedefDeclaration) {
                String targetType = ((TypedefDeclaration) declaration).getType();
                typedefs.put(declaration.getName(), targetType);
                registry.registerTypedef(
                        ref(program.getProgramName(), declaration.getName()),
                        parseType(program.getProgramName(), targetType));
            } else if (declaration instanceof ConstDeclaration) {
                TypeReference constRef = ref(program.getProgramName(), declaration.getName());
                TypeReference typeRef = parseType(program.getProgramName(), ((ConstDeclaration) declaration).getType());
                ConstValueProvider valueProvider = new ConstValueProvider(
                        registry,
                        program.getProgramName(),
                        typeRef,
                        ((ConstDeclaration) declaration).getValueTokens());
                PDescriptorProvider typeProvider = registry.getTypeProvider(typeRef);
                constants.add(new CConst(declaration.getDocumentation(),
                                         program.getProgramName(),
                                         declaration.getName(),
                                         typeProvider,
                                         valueProvider,
                                         makeAnnoatations(declaration.getAnnotations())));
                registry.registerConstant(constRef, valueProvider);
            }
        }

        try {
            for (Declaration declaration : program.getDeclarationList()) {
                TypeReference reference = ref(program.getProgramName(), declaration.getName());

                if (declaration instanceof EnumDeclaration) {
                    validateEnum((EnumDeclaration) declaration);
                } else if (declaration instanceof MessageDeclaration) {
                    validateMessage(fileName, program, registry, (MessageDeclaration) declaration);
                } else if (declaration instanceof ServiceDeclaration) {
                    PService service = registry.requireService(reference);
                    service.getExtendsService();
                    for (PServiceMethod method : service.getMethods()) {
                        method.getResponseType();
                        method.getResponseType();
                    }
                } else if (declaration instanceof TypedefDeclaration) {
                    registry.getTypeProvider(reference).descriptor();
                } else if (declaration instanceof ConstDeclaration) {
                    try {
                        registry.getConstantValue(reference);
                    } catch (UncheckedLexerException e) {
                        throw e.getCause();
                    }
                }
            }
        } catch (ThriftException e) {
            throw e;
        } catch (Exception e) {
            throw new ThriftException(e, e.getMessage());
        }

        return new CProgram(path.toString(),
                            program.getDocumentation(),
                            program.getProgramName(),
                            makeNamespaces(program.getNamespaces()),
                            getIncludedProgramNames(program),
                            program.getIncludes()
                                   .stream()
                                   .map(IncludeDeclaration::getFilePath)
                                   .collect(Collectors.toSet()),
                            typedefs.build(),
                            declaredTypes.build(),
                            services.build(),
                            constants.build());
    }

    private void validateEnum(EnumDeclaration declaration) throws ThriftException {
        Map<String, EnumValueDeclaration> forName = new HashMap<>();
        Map<Integer, EnumValueDeclaration> forId = new HashMap<>();
        for (EnumValueDeclaration val : declaration.getValues()) {
            String normalizedName = Strings.c_case(val.getName()).toUpperCase(Locale.US);
            if (forName.containsKey(normalizedName)) {
                ThriftToken otherName = forName.get(normalizedName).getNameToken();
                throw new ThriftException(val.getNameToken(), "Enum value with name already exists at line %d", otherName.lineNo());
            }
            forName.put(normalizedName, val);

            if (val.getIdToken() != null) {
                int id = val.getId();
                if (forId.containsKey(id)) {
                    ThriftToken otherId = forId.get(id).getIdToken();
                    throw new ThriftException(val.getNameToken(), "Enum value with ID %d already exists at line %d",
                                              id, otherId.lineNo());
                }

                forId.put(id, val);
            }
        }
    }

    private Map<String, String> makeNamespaces(List<NamespaceDeclaration> namespaces) {
        Map<String, String> map = new HashMap<>();
        for (NamespaceDeclaration nd : namespaces) {
            map.put(nd.getLanguage(), nd.getNamespace());
        }
        return map;
    }

    private void validateMessage(String fileName,
                                 ProgramDeclaration program,
                                 ProgramRegistry registry,
                                 MessageDeclaration mt) throws ThriftException {
        PMessageDescriptor<?> descriptor = registry.requireMessageType(
                ref(program.getProgramName(), mt.getName()));
        try {
            CInterfaceDescriptor iFace = (CInterfaceDescriptor) descriptor.getImplementing();
            if (iFace != null) {
                iFace.addPossibleType(descriptor);
            }
        } catch (ClassCastException e) {
            throw new ThriftException(mt.getImplementing(),
                                      "Bad implements type: %s is not an interface.",
                                      mt.getImplementing())
                    .setFile(fileName);

        } catch (IllegalArgumentException | IllegalStateException e) {
            // No such type.
            throw new ThriftException(mt.getImplementing(), e.getMessage())
                    .setFile(fileName);
        }

        Map<String, FieldDeclaration> forName = new HashMap<>();
        Map<Integer, FieldDeclaration> forId = new HashMap<>();
        for (PField<?> field : descriptor.getFields()) {
            FieldDeclaration ft = findField(mt.getFields(), field.getName());
            if (ft == null) throw new IllegalArgumentException("Impossible");
            String normalizedName = Strings.camelCase(ft.getName()).toUpperCase(Locale.US);
            if (forName.containsKey(normalizedName)) {
                ThriftToken other = forName.get(normalizedName).getNameToken();
                throw new ThriftException(ft.getNameToken(),
                                          "Field with name '%s' already exists on line %d",
                                          ft.getName(),
                                          other.lineNo())
                        .setFile(fileName);
            }
            forName.put(normalizedName, ft);
            if (ft.getIdToken() != null) {
                int id = ft.getId();
                if (forId.containsKey(id)) {
                    ThriftToken other = forId.get(id).getIdToken();
                    throw new ThriftException(ft.getIdToken(),
                                              "Field with id %d already exists on line %d",
                                              ft.getId(),
                                              other.lineNo())
                            .setFile(fileName);
                }
                forId.put(id, ft);
            }

            try {
                field.getDescriptor();
            } catch (IllegalArgumentException | IllegalStateException e) {
                // Unknown field type.
                ThriftToken type1 = ft.getTypeToken();
                throw new ThriftException(type1, e.getMessage())
                        .initCause(e)
                        .setFile(fileName);
            }

            try {
                field.getArgumentsType();
            } catch (IllegalArgumentException | IllegalStateException e) {
                AnnotationDeclaration args = findAnnotation(ft.getAnnotations(), PAnnotation.ARGUMENTS_TYPE);
                if (args == null || args.getValueToken() == null) throw new IllegalStateException("");
                throw new ThriftException(args.getValueToken(),
                                          e.getMessage())
                        .initCause(e)
                        .setFile(fileName);
            }

            AnnotationDeclaration refEnum = findAnnotation(ft.getAnnotations(), PAnnotation.REF_ENUM);
            if (refEnum != null) {
                ThriftToken refEnumValue = refEnum.getValueToken();
                if (refEnum.getValue().isEmpty()) {
                    throw new ThriftException(refEnumValue,
                                              "Empty type '%s' for ref.enum for '%s' in %s",
                                              refEnum.getValue(),
                                              field.getName(),
                                              descriptor.getName())
                            .setFile(fileName);
                }
                if (PPrimitive.findByName(refEnum.getValue()) != null) {
                    throw new ThriftException(refEnumValue,
                                              "Primitive type '%s' for ref.enum for '%s' in %s",
                                              refEnum.getValue(),
                                              field.getName(),
                                              descriptor.getName())
                            .setFile(fileName);
                }

                try {
                    PDeclaredDescriptor<?> dd = registry
                            .getDeclaredType(parseType(program.getProgramName(), refEnum.getValue()))
                            .orElseThrow(() -> new ThriftException(refEnumValue,
                                                                   "Unknown ref.enum type '%s' for '%s' in %s",
                                                                   refEnum, field.getName(),
                                                                   descriptor.getName())
                                    .setFile(fileName));
                    if (!(dd instanceof PEnumDescriptor)) {
                        throw new ThriftException(refEnumValue,
                                                  "'%s' is not an enum for ref.enum '%s' in %s",
                                                  refEnum.getValue(),
                                                  field.getName(),
                                                  descriptor.getName())
                                .setFile(fileName);
                    }
                } catch (IllegalArgumentException e) {
                    throw new ThriftException(refEnumValue,
                                              e.getMessage());
                }
            }
            field.getDefaultValue();
        }

        try {
            switch (descriptor.getVariant()) {
                case STRUCT: {
                    CStructDescriptor sd = (CStructDescriptor) descriptor;
                    if (sd.getImplementing() != null) {
                        CInterfaceDescriptor id = sd.getImplementing();
                        for (PField<?> iField : id.getFields()) {
                            CField<?>        found = sd.findFieldByName(iField.getName());
                            FieldDeclaration ft    = null;
                            for (FieldDeclaration f : mt.getFields()) {
                                if (f.getName().equals(iField.getName())) {
                                    ft = f;
                                    break;
                                }
                            }
                            if (found == null || ft == null) {
                                throw new ThriftException(mt.getVariantToken(),
                                                          "Missing interface field '%s' in %s implementing %s",
                                                          iField.getName(),
                                                          sd.getName(),
                                                          id.getQualifiedName(program.getProgramName()))
                                        .setFile(fileName);
                            }

                            if (!found.getDescriptor().equals(iField.getDescriptor())) {
                                throw new ThriftException(ft.getTypeToken(),
                                                          "Type mismatch for field '%s' in %s implementing %s, %s != %s",
                                                          iField.getName(),
                                                          sd.getName(),
                                                          id.getQualifiedName(program.getProgramName()),
                                                          found.getDescriptor().getQualifiedName(),
                                                          iField.getDescriptor().getQualifiedName())
                                        .setFile(fileName);
                            }
                            if (found.getRequirement() != iField.getRequirement()) {
                                throw new ThriftException(ft.getRequirementToken(),
                                                          "Requirement mismatch for field '%s' in %s implementing %s, %s != %s",
                                                          iField.getName(),
                                                          sd.getName(),
                                                          id.getQualifiedName(program.getProgramName()),
                                                          found.getRequirement(),
                                                          iField.getRequirement())
                                        .setFile(fileName);
                            }
                        }
                    }
                    break;
                }
                case UNION: {
                    CUnionDescriptor sd = (CUnionDescriptor) descriptor;
                    if (sd.getImplementing() != null) {
                        CInterfaceDescriptor id = sd.getImplementing();

                        for (CField<?> field : sd.getFields()) {
                            FieldDeclaration cft = mt.getFields()
                                              .stream().filter(f -> f.getName().equals(field.getName()))
                                              .findFirst()
                                              .orElseThrow(() -> new IllegalStateException("Unable to find field source"));

                            PDescriptor pd = field.getDescriptor();
                            if (pd.getType() != PType.MESSAGE) {
                                throw new ThriftException(cft.getTypeToken(),
                                                          "Field %s in union %s of %s is not a message.",
                                                          field.getName(),
                                                          sd.getQualifiedName(),
                                                          id.getQualifiedName(program.getProgramName()))
                                        .setFile(fileName);
                            }
                            PMessageDescriptor<?> pmd = (PMessageDescriptor<?>) pd;
                            if (pmd.getImplementing() == null ||
                                !pmd.getImplementing().equals(id)) {
                                throw new ThriftException(cft.getTypeToken(),
                                                          "Field '%s' in union %s of %s does not implement required interface.",
                                                          field.getName(),
                                                          sd.getQualifiedName(),
                                                          id.getQualifiedName(program.getProgramName()))
                                        .setFile(fileName);
                            }
                        }
                    }
                    break;
                }
            }
        } catch (IllegalArgumentException | IllegalStateException e) {
            throw new ThriftException(mt.getVariantToken(), e.getMessage())
                    .initCause(e)
                    .setFile(fileName);
        }
    }

    private void registerService(String path,
                                 ProgramDeclaration program,
                                 ServiceDeclaration serviceType,
                                 UnmodifiableList.Builder<CService> services,
                                 ProgramRegistry registry)
            throws ThriftException {
        UnmodifiableList.Builder<CServiceMethod> methodBuilder = UnmodifiableList.builder();

        TypeReference serviceRef = ref(program.getProgramName(), serviceType.getName());

        for (MethodDeclaration sm : serviceType.getMethods()) {
            List<CField<CStruct>> rqFields = new ArrayList<>();
            CStructDescriptor request;
            boolean protoStub = false;
            if (sm.getRequestTypeToken() != null) {
                TypeReference requestRef = ref(program.getProgramName(), sm.getRequestTypeToken().toString());
                try {
                    PDescriptor descriptor = globalRegistry.requireDeclaredType(requestRef);

                    if (!(descriptor instanceof CStructDescriptor)) {
                        throw new ThriftException(
                                sm.getRequestTypeToken(),
                                "Not a struct type for proto stub method request type");
                    }
                    request = (CStructDescriptor) descriptor;
                    protoStub = true;
                } catch (IllegalArgumentException e) {
                    // Not a declared type.
                    throw new ThriftException(sm.getRequestTypeToken(), e.getMessage()).initCause(e);
                }
            } else {
                for (FieldDeclaration field : sm.getParams()) {
                    rqFields.add(makeField(registry,
                                           path,
                                           program.getProgramName(),
                                           field,
                                           PMessageVariant.STRUCT,
                                           null));
                }

                request = new CStructDescriptor(null,
                                                program.getProgramName(),
                                                serviceType.getName() + '.' +
                                                sm.getName() + ".request",
                                                rqFields,
                                                null,
                                                null);
            }

            CUnionDescriptor response = null;
            if (!sm.isOneWay()) {
                List<CField<CUnion>> rsFields = new ArrayList<>();
                CField<CUnion> success = new CField<>(
                        null,
                        0,
                        PRequirement.OPTIONAL,
                        "success",
                        registry.getTypeProvider(parseType(program.getProgramName(), sm.getReturnType())),
                        null,
                        null,
                        makeAnnoatations(sm.getAnnotations()),
                        null);
                if (protoStub && !(success.getDescriptor() instanceof PStructDescriptor)) {
                    throw new ThriftException(
                            // TODO: Point to whole of return type?
                            sm.getReturnTypeTokens().get(0),
                            "Response type not a struct on proto stub method");
                }
                rsFields.add(success);

                if (sm.getThrowing() != null) {
                    for (FieldDeclaration field : sm.getThrowing()) {
                        rsFields.add(makeField(registry,
                                               path,
                                               program.getProgramName(),
                                               field,
                                               PMessageVariant.UNION,
                                               null));
                    }
                }

                response = new CUnionDescriptor(null,
                                                program.getProgramName(),
                                                serviceType.getName() + '.' +
                                                sm.getName() + ".response",
                                                rsFields,
                                                null,
                                                null);
            } else if (protoStub) {
                throw new ThriftException(
                        sm.getRequestTypeToken(),
                        "Proto stubs may not be oneway");
            }

            CServiceMethod method = new CServiceMethod(
                    sm.getDocumentation(),
                    sm.getName(),
                    sm.isOneWay(),
                    protoStub,
                    request,
                    response,
                    makeAnnoatations(sm.getAnnotations()),
                    registry.getServiceProvider(serviceRef));

            methodBuilder.add(method);
        }  // for each method

        PServiceProvider extendsProvider = null;
        if (serviceType.getExtending() != null) {
            extendsProvider = registry.getServiceProvider(
                    parseType(program.getProgramName(), serviceType.getExtending()));
        }

        CService service = new CService(serviceType.getDocumentation(),
                                        program.getProgramName(),
                                        serviceType.getName(),
                                        extendsProvider,
                                        methodBuilder.build(),
                                        makeAnnoatations(serviceType.getAnnotations()));

        services.add(service);
        registry.registerService(service);
    }

    private void registerMessage(String path,
                                 ProgramDeclaration program,
                                 MessageDeclaration messageType,
                                 UnmodifiableList.Builder<PDeclaredDescriptor<?>> declaredTypes,
                                 ProgramRegistry registry) throws ThriftException {
        PDescriptorProvider implementing = null;
        if (messageType.getImplementing() != null) {
            implementing = registry.getTypeProvider(
                    parseType(program.getProgramName(), messageType.getImplementing().toString()));
        }

        PMessageDescriptor<?> type;
        switch (messageType.getVariant()) {
            case STRUCT: {
                List<CField<CStruct>> fields = new ArrayList<>();
                for (FieldDeclaration field : messageType.getFields()) {
                    fields.add(makeField(registry,
                                         path,
                                         program.getProgramName(),
                                         field,
                                         messageType.getVariant(),
                                         implementing));
                }
                type = new CStructDescriptor(messageType.getDocumentation(),
                                             program.getProgramName(),
                                             messageType.getName(),
                                             fields,
                                             makeAnnoatations(messageType.getAnnotations()),
                                             implementing);
                break;
            }
            case UNION: {
                List<CField<CUnion>> fields = new ArrayList<>();
                for (FieldDeclaration field : messageType.getFields()) {
                    fields.add(makeField(registry,
                                         path,
                                         program.getProgramName(),
                                         field,
                                         messageType.getVariant(),
                                         implementing));
                }
                type = new CUnionDescriptor(messageType.getDocumentation(),
                                            program.getProgramName(),
                                            messageType.getName(),
                                            fields,
                                            makeAnnoatations(messageType.getAnnotations()),
                                            implementing);
                break;
            }
            case EXCEPTION: {
                List<CField<CException>> fields = new ArrayList<>();
                for (FieldDeclaration field : messageType.getFields()) {
                    fields.add(makeField(registry,
                                         path,
                                         program.getProgramName(),
                                         field,
                                         messageType.getVariant(),
                                         implementing));
                }
                type = new CExceptionDescriptor(messageType.getDocumentation(),
                                                program.getProgramName(),
                                                messageType.getName(),
                                                fields,
                                                makeAnnoatations(messageType.getAnnotations()));
                break;
            }
            case INTERFACE: {
                List<CField<CInterface>> fields = new ArrayList<>();
                for (FieldDeclaration field : messageType.getFields()) {
                    fields.add(makeField(registry,
                                         path,
                                         program.getProgramName(),
                                         field,
                                         messageType.getVariant(),
                                         implementing));
                }
                type = new CInterfaceDescriptor(messageType.getDocumentation(),
                                                program.getProgramName(),
                                                messageType.getName(),
                                                fields,
                                                makeAnnoatations(messageType.getAnnotations()));
                break;
            }
            default:
                throw new UnsupportedOperationException(
                        "Unhandled message variant " + messageType.getVariant());
        }
        declaredTypes.add(type);
        registry.registerType(type);
    }

    private void registerEnum(ProgramDeclaration program,
                              EnumDeclaration enumType,
                              UnmodifiableList.Builder<PDeclaredDescriptor<?>> declaredTypes,
                              ProgramRegistry registry) {
        int nextValue = PEnumDescriptor.DEFAULT_FIRST_VALUE;
        CEnumDescriptor type = new CEnumDescriptor(enumType.getDocumentation(),
                                                   program.getProgramName(),
                                                   enumType.getName(),
                                                   makeAnnoatations(enumType.getAnnotations()));
        List<CEnumValue> values = new ArrayList<>();
        for (EnumValueDeclaration value : enumType.getValues()) {
            int v = value.getId() > 0 ? value.getId() : nextValue;
            nextValue = v + 1;
            values.add(new CEnumValue(value.getDocumentation(),
                                      value.getId(),
                                      value.getName(),
                                      type,
                                      makeAnnoatations(value.getAnnotations())));
        }
        type.setValues(values);
        declaredTypes.add(type);
        registry.registerType(type);
    }

    private FieldDeclaration findField(Collection<FieldDeclaration> fields, String name) {
        for (FieldDeclaration field : fields) {
            if (field.getName().equals(name)) {
                return field;
            }
        }
        return null;
    }

    private Set<String> getIncludedProgramNames(ProgramDeclaration document) throws ThriftException {
        Set<String> out = new TreeSet<>();
        for (IncludeDeclaration include : document.getIncludes()) {
            String program = include.getProgramName();
            if (out.contains(program)) {
                throw new ThriftException(
                        include.getProgramNameToken() != null ? include.getProgramNameToken() : include.getFilePathToken(),
                        "Including multiple programs of name " + program);
            }
            out.add(program);
        }
        return out;
    }

    @SuppressWarnings("rawtypes")
    private <M extends PMessage<M>>
    CField<M> makeField(@Nonnull TypeRegistry registry,
                        @Nonnull String fileName,
                        @Nonnull String programName,
                        @Nonnull FieldDeclaration field,
                        @Nonnull PMessageVariant variant,
                        @Nullable PDescriptorProvider implementing)
            throws ThriftException {
        TypeReference reference = parseType(programName, field.getType());
        PDescriptorProvider type = registry.getTypeProvider(
                reference, makeAnnoatations(field.getAnnotations()));
        ConstValueProvider defaultValue = null;
        if (field.getDefaultValueTokens() != null) {
            defaultValue = new ConstValueProvider(registry,
                                                  programName,
                                                  reference,
                                                  field.getDefaultValueTokens());
        }

        PStructDescriptorProvider<?> argumentsProvider = null;
        AnnotationDeclaration     arguments         = findAnnotation(field.getAnnotations(), PAnnotation.ARGUMENTS_TYPE);
        if (arguments != null) {
            if (arguments.getValue().isEmpty()) {
                throw new ThriftException(arguments.getValueToken(),
                                          "Empty " + PAnnotation.ARGUMENTS_TYPE.tag + " annotation.")
                        .setFile(fileName);
            }
            if (PPrimitive.findByName(arguments.getValue()) != null) {
                throw new ThriftException(arguments.getValueToken(),
                        "Primitive " + arguments.getValue() + " not allowed as argument type")
                        .setFile(fileName);
            }
            PDescriptorProvider desc = registry.getTypeProvider(parseType(programName, arguments.getValue()),
                                                                makeAnnoatations(field.getAnnotations()));
            argumentsProvider = new PStructDescriptorProvider() {
                @Nonnull
                @Override
                public PStructDescriptor<?> descriptor() {
                    try {
                        return (PStructDescriptor<?>) desc.descriptor();
                    } catch (IllegalArgumentException e) {
                        throw new IllegalArgumentException(
                                e.getMessage()
                                 .replace(" in program '", " for argument type in program '"),
                                e);
                    } catch (ClassCastException e) {
                        throw new IllegalStateException(
                                "'" + arguments.getValue() + "' is not a struct for argument type in program '" + programName + "'", e);
                    }
                }
            };
        }

        AnnotationDeclaration refEnum = findAnnotation(field.getAnnotations(), PAnnotation.REF_ENUM);
        if (refEnum != null && refEnum.getValue().isEmpty()) {
            throw new ThriftException(refEnum.getValueToken(),
                                      "Empty " + PAnnotation.REF_ENUM.tag + " annotation.")
                    .setFile(fileName);
        }

        AnnotationDeclaration container = findAnnotation(field.getAnnotations(), PAnnotation.CONTAINER);
        if (container != null) {
            if (container.getValue().isEmpty()) {
                throw new ThriftException(container.getValueToken(),
                                          "Empty " + PAnnotation.CONTAINER.tag + " annotation.")
                        .setFile(fileName);
            }
            if (!PContainer.isValid(container.getValue())) {
                throw new ThriftException(container.getValueToken(),
                                          "Invalid " + PAnnotation.CONTAINER.tag + " annotation," +
                                          " must be one of 'ordered', 'sorted' or 'default'.")
                        .setFile(fileName);
            }
        }

        PRequirement requirement = field.getRequirement();
        if (variant == PMessageVariant.UNION) {
            if (requirement == PRequirement.REQUIRED) {
                throw new ThriftException(
                        field.getRequirementToken(), "Required field declaration in union");
            }
            requirement = PRequirement.OPTIONAL;
        }
        return new CField<>(field.getDocumentation(),
                            field.getId(),
                            requirement,
                            field.getName(),
                            type,
                            argumentsProvider,
                            defaultValue,
                            makeAnnoatations(field.getAnnotations()),
                            implementing);
    }

    private AnnotationDeclaration findAnnotation(List<AnnotationDeclaration> annotations, PAnnotation annotation) {
        for (AnnotationDeclaration annotationDeclaration : annotations) {
            if (annotationDeclaration.getTag().equals(annotation.tag)) {
                return annotationDeclaration;
            }
        }
        return null;
    }

    private Map<String, String> makeAnnoatations(List<AnnotationDeclaration> annotations) {
        Map<String, String> map = new HashMap<>();
        for (AnnotationDeclaration annotation : annotations) {
            map.put(annotation.getTag(), annotation.getValue());
        }
        return map;
    }
}