All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.smithy.diff.evaluators.ChangedNullability Maven / Gradle / Ivy

Go to download

This module detects differences between two Smithy models, identifying changes that are safe and changes that are backward incompatible.

There is a newer version: 1.53.0
Show newest version
/*
 * Copyright 2022 Amazon.com, Inc. or its affiliates. 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.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 software.amazon.smithy.diff.evaluators;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import software.amazon.smithy.diff.ChangedShape;
import software.amazon.smithy.diff.Differences;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.NullableIndex;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.traits.ClientOptionalTrait;
import software.amazon.smithy.model.traits.DefaultTrait;
import software.amazon.smithy.model.traits.InputTrait;
import software.amazon.smithy.model.traits.RequiredTrait;
import software.amazon.smithy.model.validation.Severity;
import software.amazon.smithy.model.validation.ValidationEvent;

/**
 * Validates that only backward compatible changes are made to
 * structure member nullability to ensure that if something was
 * previously nullable to clients then it continue to be nullable
 * and vice versa.
 */
public class ChangedNullability extends AbstractDiffEvaluator {
    @Override
    public List evaluate(Differences differences) {
        NullableIndex oldIndex = NullableIndex.of(differences.getOldModel());
        NullableIndex newIndex = NullableIndex.of(differences.getNewModel());
        Set events = new HashSet<>();

        Stream.concat(
            // Get members that changed.
            differences.changedShapes(MemberShape.class),
            // Get members of structures that added/removed the input trait.
            changedInputMembers(differences)
        ).forEach(change -> {
            // If NullableIndex says the nullability of a member changed, then that's a breaking change.
            MemberShape oldShape = change.getOldShape();
            MemberShape newShape = change.getNewShape();
            boolean wasNullable = oldIndex.isMemberNullable(oldShape);
            boolean isNowNullable = newIndex.isMemberNullable(newShape);
            if (wasNullable != isNowNullable) {
                createErrors(differences, change, wasNullable, events);
            }
        });

        return new ArrayList<>(events);
    }

    private Stream> changedInputMembers(Differences differences) {
        return differences.changedShapes(StructureShape.class)
                .filter(change -> change.isTraitAdded(InputTrait.ID) || change.isTraitRemoved(InputTrait.ID))
                // Find all members that existed before and exist now.
                .flatMap(change -> change.getNewShape().members().stream()
                        .map(newMember -> {
                            MemberShape old = change.getOldShape().getAllMembers().get(newMember.getMemberName());
                            return old == null ? null : new ChangedShape<>(old, newMember);
                        })
                        .filter(Objects::nonNull));
    }

    private void createErrors(
            Differences differences,
            ChangedShape change,
            boolean wasNullable,
            Collection events
    ) {
        MemberShape oldMember = change.getOldShape();
        MemberShape newMember = change.getNewShape();
        String message = String.format("Member `%s` changed from %s to %s: ",
                                       oldMember.getMemberName(),
                                       wasNullable ? "nullable" : "non-nullable",
                                       wasNullable ? "non-nullable" : "nullable");
        boolean oldHasInput = hasInputTrait(differences.getOldModel(), oldMember);
        boolean newHasInput = hasInputTrait(differences.getNewModel(), newMember);
        ShapeId shape = change.getShapeId();
        Shape newTarget = differences.getNewModel().expectShape(newMember.getTarget());
        int currentEventSize = events.size();

        if (oldHasInput && !newHasInput) {
            // If there was an input trait before, but not now, then the nullability must have
            // changed from nullable to non-nullable.
            events.add(emit(Severity.ERROR, "RemovedInputTrait", shape, message,
                            "The @input trait was removed from " + newMember.getContainer()));
        } else if (!oldHasInput && newHasInput) {
            // If there was no input trait before, but there is now, then the nullability must have
            // changed from non-nullable to nullable.
            events.add(emit(Severity.DANGER, "AddedInputTrait", shape, message,
                            "The @input trait was added to " + newMember.getContainer()));
        } else if (!newHasInput) {
            // Can't add nullable to a preexisting required member.
            if (change.isTraitAdded(ClientOptionalTrait.ID) && change.isTraitInBoth(RequiredTrait.ID)) {
                events.add(emit(Severity.ERROR, "AddedNullableTrait", shape, message,
                                "The @nullable trait was added to a @required member."));
            }
            // Can't add required to a member unless the member is marked as nullable.
            if (change.isTraitAdded(RequiredTrait.ID) && !newMember.hasTrait(ClientOptionalTrait.ID)) {
                events.add(emit(Severity.ERROR, "AddedRequiredTrait", shape, message,
                                "The @required trait was added to a member that is not marked as @nullable."));
            }
            // Can't add the default trait to a member unless the member was previously required.
            if (change.isTraitAdded(DefaultTrait.ID) && !change.isTraitRemoved(RequiredTrait.ID)) {
                events.add(emit(Severity.ERROR, "AddedDefaultTrait", shape, message,
                                "The @default trait was added to a member that was not previously @required."));
            }
            // Can only remove the required trait if the member was nullable or replaced by the default trait.
            if (change.isTraitRemoved(RequiredTrait.ID)
                    && !newMember.hasTrait(DefaultTrait.ID)
                    && !oldMember.hasTrait(ClientOptionalTrait.ID)) {
                if (newTarget.isStructureShape() || newTarget.isUnionShape()) {
                    events.add(emit(Severity.WARNING, "RemovedRequiredTrait.StructureOrUnion", shape, message,
                                    "The @required trait was removed from a member that targets a "
                                    + newTarget.getType() + ". This is backward compatible in generators that "
                                    + "always treat structures and unions as optional (e.g., AWS generators)"));
                } else {
                    events.add(emit(Severity.ERROR, "RemovedRequiredTrait", shape, message,
                                    "The @required trait was removed and not replaced with the @default trait and "
                                    + "@addedDefault trait."));
                }
            }
        }

        // If not specific event was emitted, emit a generic event.
        if (events.size() == currentEventSize) {
            events.add(emit(Severity.ERROR, null, shape, null, message));
        }
    }

    private boolean hasInputTrait(Model model, MemberShape member) {
        return model.getShape(member.getContainer()).filter(shape -> shape.hasTrait(InputTrait.ID)).isPresent();
    }

    private ValidationEvent emit(
            Severity severity,
            String eventIdSuffix,
            ShapeId shape,
            String prefixMessage,
            String message
    ) {
        String actualId = eventIdSuffix == null ? getEventId() : (getEventId() + '.' + eventIdSuffix);
        String actualMessage = prefixMessage == null ? message : (prefixMessage + "; " + message);
        return ValidationEvent.builder()
                .id(actualId)
                .shapeId(shape)
                .message(actualMessage)
                .severity(severity)
                .build();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy