Skip to content

Commit 1272e4d

Browse files
Loosen ChangedMemberTarget evaluator
This updates the ChangedMemberTarget diff evaulator to emit a warning instead of an error when the new target has a different set of traits. It now is expected to only emit an error when the new target is not expected to be generated as a compatible type. The messages generally have been updated. It now generates messages about traits by generating a syntehtic ChangedShape and calling ModelDiff on that. This is also how it deals with knowing whether a trait change is compatible or not - it just checks if there's any DANGER or worse events in the new set of events.
1 parent fa6a9cd commit 1272e4d

File tree

34 files changed

+645
-164
lines changed

34 files changed

+645
-164
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "feature",
3+
"description": "Loosened the `ChangedMemberTarget` diff evaluator to not report `ERROR` level events so often. Now it will always report an `ERROR` if the change is expected to result in codegen type errors, but otherwise it will default to `WARNING` unless differing traits between the targets would result in a higher severity event had those trait changes been applied to the original target.",
4+
"pull_requests": [
5+
"[#2796](https://github.com/smithy-lang/smithy/pull/2796)"
6+
]
7+
}

smithy-diff/src/main/java/software/amazon/smithy/diff/ChangedShape.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public SourceLocation getSourceLocation() {
6363
* @return Return the shape ID.
6464
*/
6565
public ShapeId getShapeId() {
66-
return newShape.getId();
66+
return oldShape.getId();
6767
}
6868

6969
/**

smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/ChangedMemberTarget.java

Lines changed: 135 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,42 @@
77
import java.util.ArrayList;
88
import java.util.List;
99
import java.util.Set;
10-
import java.util.StringJoiner;
11-
import java.util.TreeSet;
1210
import java.util.stream.Collectors;
1311
import software.amazon.smithy.diff.ChangedShape;
1412
import software.amazon.smithy.diff.Differences;
13+
import software.amazon.smithy.diff.ModelDiff;
1514
import software.amazon.smithy.model.Model;
1615
import software.amazon.smithy.model.shapes.CollectionShape;
1716
import software.amazon.smithy.model.shapes.MapShape;
1817
import software.amazon.smithy.model.shapes.MemberShape;
1918
import software.amazon.smithy.model.shapes.Shape;
2019
import software.amazon.smithy.model.shapes.ShapeId;
21-
import software.amazon.smithy.model.shapes.ShapeType;
2220
import software.amazon.smithy.model.shapes.SimpleShape;
2321
import software.amazon.smithy.model.traits.EnumTrait;
24-
import software.amazon.smithy.model.traits.Trait;
2522
import software.amazon.smithy.model.validation.Severity;
2623
import software.amazon.smithy.model.validation.ValidationEvent;
27-
import software.amazon.smithy.utils.ListUtils;
24+
import software.amazon.smithy.utils.Pair;
2825
import software.amazon.smithy.utils.SetUtils;
26+
import software.amazon.smithy.utils.StringUtils;
2927

3028
/**
3129
* Checks for changes in the shapes targeted by a member.
3230
*
33-
* <p>If the shape targeted by the member changes from a simple shape to
34-
* a simple shape of the same type with the same traits, or a list or set
35-
* that has a member that targets the shame exact shape and has the same
36-
* traits, then the emitted event is a WARNING. If an enum trait is
37-
* found on the old or newly targeted shape, then the event is an ERROR,
38-
* because enum traits typically materialize as named types in codegen.
39-
* All other changes are ERROR events.
31+
* <p>If the new target is not a compatible type, the emitted event will be
32+
* an ERROR. The new target is not compatible if any of the following are true:
33+
*
34+
* <ul>
35+
* <li>The new target is a different shape type than the old target.</li>
36+
* <li>The target is a shape type whose name is significant to code generation,
37+
* such as structures and enums.</li>
38+
* <li>The new target is a list whose member is not a compatible type with the
39+
* old target's member.</li>
40+
* <li>The new target is a map whose key or value is not a compatible type with
41+
* the old target's key or value.</li>
42+
* </ul>
43+
*
44+
* <p>If the types are compatible, the emitted event will default to a WARNING. This
45+
* is elevated if any trait changes would result in a higher severity.
4046
*/
4147
public final class ChangedMemberTarget extends AbstractDiffEvaluator {
4248

@@ -47,29 +53,49 @@ public final class ChangedMemberTarget extends AbstractDiffEvaluator {
4753
*/
4854
private static final Set<ShapeId> SIGNIFICANT_CODEGEN_TRAITS = SetUtils.of(EnumTrait.ID);
4955

56+
private static final Pair<Severity, String> COMPATIBLE =
57+
Pair.of(Severity.WARNING, "This was determined backward compatible.");
58+
5059
@Override
5160
public List<ValidationEvent> evaluate(Differences differences) {
61+
return evaluate(ChangedMemberTarget.class.getClassLoader(), differences);
62+
}
63+
64+
@Override
65+
public List<ValidationEvent> evaluate(ClassLoader classLoader, Differences differences) {
5266
return differences.changedShapes(MemberShape.class)
5367
.filter(change -> !change.getOldShape().getTarget().equals(change.getNewShape().getTarget()))
54-
.map(change -> createChangeEvent(differences, change))
68+
.map(change -> createChangeEvent(classLoader, differences, change))
5569
.collect(Collectors.toList());
5670
}
5771

58-
private ValidationEvent createChangeEvent(Differences differences, ChangedShape<MemberShape> change) {
59-
Shape oldTarget = getShapeTarget(differences.getOldModel(), change.getOldShape().getTarget());
60-
Shape newTarget = getShapeTarget(differences.getNewModel(), change.getNewShape().getTarget());
61-
List<String> issues = areShapesCompatible(oldTarget, newTarget);
62-
Severity severity = issues.isEmpty() ? Severity.WARNING : Severity.ERROR;
63-
64-
String message = createSimpleMessage(change, oldTarget, newTarget);
65-
if (severity == Severity.WARNING) {
66-
message += "This was determined backward compatible.";
67-
} else {
68-
message += String.join(". ", issues) + ".";
69-
}
72+
private ValidationEvent createChangeEvent(
73+
ClassLoader classLoader,
74+
Differences differences,
75+
ChangedShape<MemberShape> change
76+
) {
77+
return createChangeEvent(classLoader, differences.getOldModel(), differences.getNewModel(), change);
78+
}
79+
80+
private ValidationEvent createChangeEvent(
81+
ClassLoader classLoader,
82+
Model oldModel,
83+
Model newModel,
84+
ChangedShape<MemberShape> change
85+
) {
86+
Shape oldTarget = getShapeTarget(oldModel, change.getOldShape().getTarget());
87+
Shape newTarget = getShapeTarget(newModel, change.getNewShape().getTarget());
88+
89+
Pair<Severity, String> evaluation = evaluateShape(
90+
classLoader,
91+
oldModel,
92+
newModel,
93+
oldTarget,
94+
newTarget);
95+
String message = createSimpleMessage(change, oldTarget, newTarget) + evaluation.getRight();
7096

7197
return ValidationEvent.builder()
72-
.severity(severity)
98+
.severity(evaluation.getLeft())
7399
.id(getEventId())
74100
.shape(change.getNewShape())
75101
.message(message)
@@ -80,77 +106,108 @@ private Shape getShapeTarget(Model model, ShapeId id) {
80106
return model.getShape(id).orElse(null);
81107
}
82108

83-
private static List<String> areShapesCompatible(Shape oldShape, Shape newShape) {
109+
private Pair<Severity, String> evaluateShape(
110+
ClassLoader classLoader,
111+
Model oldModel,
112+
Model newModel,
113+
Shape oldShape,
114+
Shape newShape
115+
) {
84116
if (oldShape == null || newShape == null) {
85-
return ListUtils.of();
117+
return COMPATIBLE;
86118
}
87119

88120
if (oldShape.getType() != newShape.getType()) {
89-
return ListUtils.of(String.format("The type of the targeted shape changed from %s to %s",
90-
oldShape.getType(),
91-
newShape.getType()));
121+
return Pair.of(
122+
Severity.ERROR,
123+
String.format(
124+
"The type of the targeted shape changed from %s to %s.",
125+
oldShape.getType(),
126+
newShape.getType()));
92127
}
93128

94-
if (!(oldShape instanceof SimpleShape || oldShape instanceof CollectionShape || oldShape instanceof MapShape)) {
95-
return ListUtils.of(String.format("The name of a %s is significant", oldShape.getType()));
129+
if (!(oldShape instanceof SimpleShape || oldShape instanceof CollectionShape || oldShape.isMapShape())
130+
|| oldShape.isIntEnumShape()
131+
|| oldShape.isEnumShape()) {
132+
return Pair.of(
133+
Severity.ERROR,
134+
String.format("The name of a %s is significant.", oldShape.getType()));
96135
}
97136

98-
List<String> results = new ArrayList<>();
99137
for (ShapeId significantCodegenTrait : SIGNIFICANT_CODEGEN_TRAITS) {
100138
if (oldShape.hasTrait(significantCodegenTrait)) {
101-
results.add(String.format("The `%s` trait was found on the target, so the name of the targeted "
102-
+ "shape matters for codegen",
103-
significantCodegenTrait));
139+
return Pair.of(
140+
Severity.ERROR,
141+
String.format("The `%s` trait was found on the target, so the name of the targeted "
142+
+ "shape matters for codegen.",
143+
significantCodegenTrait));
104144
}
105145
}
106146

107-
if (!oldShape.getAllTraits().equals(newShape.getAllTraits())) {
108-
results.add(createTraitDiffMessage(oldShape, newShape));
109-
}
110-
147+
// Now that we've checked several terminal conditions, we need to evaluate traits and
148+
// collection/map member targets. To evaluate traits, we will create a synthetic diff
149+
// set to re-run the diff evaluator on. That will ensure that any differences are
150+
// given the proper severity and context rather than simply returning an ERROR for any
151+
// difference.
152+
Differences.Builder differences = Differences.builder()
153+
.oldModel(oldModel)
154+
.newModel(newModel)
155+
.changedShape(new ChangedShape<>(oldShape, newShape));
156+
157+
// Add any list / map members to the set of differences to check, and potentially
158+
// recurse if this evaluator needs to be run on them. Note that this can't recurse
159+
// infinitely, even without any specific checks here. That's because to get to this
160+
// point a member target had to change without changing shape type and without being
161+
// a structure, union, or enum. Neither maps nor lists can recurse by themselves or
162+
// with each other, there MUST be a structure or union in the path for recursion to
163+
// happen in a way that Smithy will allow. Therefore, when the structure or union
164+
// in the path is hit, it'll get caught in the terminal conditions above.
111165
if (oldShape instanceof CollectionShape) {
112-
evaluateMember(oldShape.getType(),
113-
results,
114-
((CollectionShape) oldShape).getMember(),
115-
((CollectionShape) newShape).getMember());
166+
MemberShape oldMember = ((CollectionShape) oldShape).getMember();
167+
MemberShape newMember = ((CollectionShape) newShape).getMember();
168+
differences.changedShape(new ChangedShape<>(oldMember, newMember));
116169
} else if (oldShape instanceof MapShape) {
117-
MapShape oldMapShape = (MapShape) oldShape;
118-
MapShape newMapShape = (MapShape) newShape;
119-
// Both the key and value need to be evaluated for maps.
120-
evaluateMember(oldShape.getType(),
121-
results,
122-
oldMapShape.getKey(),
123-
newMapShape.getKey());
124-
evaluateMember(oldShape.getType(),
125-
results,
126-
oldMapShape.getValue(),
127-
newMapShape.getValue());
170+
MapShape oldMap = (MapShape) oldShape;
171+
MapShape newMap = (MapShape) newShape;
172+
differences.changedShape(new ChangedShape<>(oldMap.getKey(), newMap.getKey()));
173+
differences.changedShape(new ChangedShape<>(oldMap.getValue(), newMap.getValue()));
128174
}
129175

130-
return results;
131-
}
176+
// Re-run the diff evaluator with this changed shape and any changed members.
177+
ModelDiff.Result result = ModelDiff.builder()
178+
.oldModel(oldModel)
179+
.newModel(newModel)
180+
.classLoader(classLoader)
181+
.compare(differences.build());
182+
List<ValidationEvent> diffEvents = new ArrayList<>(result.getDiffEvents());
132183

133-
private static void evaluateMember(
134-
ShapeType oldShapeType,
135-
List<String> results,
136-
MemberShape oldMember,
137-
MemberShape newMember
138-
) {
139-
String memberSlug = oldShapeType == ShapeType.MAP ? oldMember.getMemberName() + " " : "";
140-
if (!oldMember.getTarget().equals(newMember.getTarget())) {
141-
results.add(String.format("Both the old and new shapes are a %s, but the old shape %stargeted "
142-
+ "`%s` while the new shape targets `%s`",
143-
oldShapeType,
144-
memberSlug,
145-
oldMember.getTarget(),
146-
newMember.getTarget()));
147-
} else if (!oldMember.getAllTraits().equals(newMember.getAllTraits())) {
148-
results.add(String.format("Both the old and new shapes are a %s, but their %smembers have "
149-
+ "differing traits. %s",
150-
oldShapeType,
151-
memberSlug,
152-
createTraitDiffMessage(oldMember, newMember)));
184+
if (diffEvents.isEmpty()) {
185+
return COMPATIBLE;
153186
}
187+
188+
Severity severity = Severity.WARNING;
189+
StringBuilder message = new StringBuilder("This will result in the following effective differences:\n\n");
190+
191+
for (ValidationEvent event : diffEvents) {
192+
// If the severity in any event is greater than the current severity, elevate it
193+
// to that level.
194+
severity = severity.compareTo(event.getSeverity()) > 0 ? severity : event.getSeverity();
195+
196+
// Add the event to a list and indent the message in case it also spans
197+
// multiple lines.
198+
message.append("- [")
199+
.append(event.getSeverity())
200+
.append("] ")
201+
.append(StringUtils.indent(event.getMessage(), 2).trim())
202+
.append("\n");
203+
}
204+
205+
// If there are only warnings or less,
206+
if (severity.compareTo(Severity.WARNING) <= 0) {
207+
message.insert(0, "This was determined backward compatible. ");
208+
}
209+
210+
return Pair.of(severity, message.toString().trim());
154211
}
155212

156213
private static String createSimpleMessage(ChangedShape<MemberShape> change, Shape oldTarget, Shape newTarget) {
@@ -162,36 +219,4 @@ private static String createSimpleMessage(ChangedShape<MemberShape> change, Shap
162219
change.getNewShape().getTarget(),
163220
newTarget.getType());
164221
}
165-
166-
private static String createTraitDiffMessage(Shape oldShape, Shape newShape) {
167-
StringJoiner joiner = new StringJoiner(". ");
168-
ChangedShape<Shape> targetChange = new ChangedShape<>(oldShape, newShape);
169-
170-
Set<ShapeId> removedTraits = targetChange.removedTraits()
171-
.map(Trait::toShapeId)
172-
.collect(Collectors.toCollection(TreeSet::new));
173-
174-
if (!removedTraits.isEmpty()) {
175-
joiner.add("The targeted shape no longer has the following traits: " + removedTraits);
176-
}
177-
178-
Set<ShapeId> addedTraits = targetChange.addedTraits()
179-
.map(Trait::toShapeId)
180-
.collect(Collectors.toCollection(TreeSet::new));
181-
182-
if (!addedTraits.isEmpty()) {
183-
joiner.add("The newly targeted shape now has the following additional traits: " + addedTraits);
184-
}
185-
186-
// Only select the traits that exist in both placed but changed.
187-
Set<ShapeId> changedTraits = new TreeSet<>(targetChange.getTraitDifferences().keySet());
188-
changedTraits.removeAll(addedTraits);
189-
changedTraits.removeAll(removedTraits);
190-
191-
if (!changedTraits.isEmpty()) {
192-
joiner.add("The newly targeted shape has traits that differ from the previous shape: " + changedTraits);
193-
}
194-
195-
return joiner.toString();
196-
}
197222
}

0 commit comments

Comments
 (0)