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