77import java .util .ArrayList ;
88import java .util .List ;
99import java .util .Set ;
10- import java .util .StringJoiner ;
11- import java .util .TreeSet ;
1210import java .util .stream .Collectors ;
1311import software .amazon .smithy .diff .ChangedShape ;
1412import software .amazon .smithy .diff .Differences ;
13+ import software .amazon .smithy .diff .ModelDiff ;
1514import software .amazon .smithy .model .Model ;
1615import software .amazon .smithy .model .shapes .CollectionShape ;
1716import software .amazon .smithy .model .shapes .MapShape ;
1817import software .amazon .smithy .model .shapes .MemberShape ;
1918import software .amazon .smithy .model .shapes .Shape ;
2019import software .amazon .smithy .model .shapes .ShapeId ;
21- import software .amazon .smithy .model .shapes .ShapeType ;
2220import software .amazon .smithy .model .shapes .SimpleShape ;
2321import software .amazon .smithy .model .traits .EnumTrait ;
24- import software .amazon .smithy .model .traits .Trait ;
2522import software .amazon .smithy .model .validation .Severity ;
2623import software .amazon .smithy .model .validation .ValidationEvent ;
27- import software .amazon .smithy .utils .ListUtils ;
24+ import software .amazon .smithy .utils .Pair ;
2825import 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 */
4147public 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