1 /*
2  * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */
15 
16 package software.amazon.smithy.go.codegen;
17 
18 import java.util.HashMap;
19 import java.util.Map;
20 import java.util.Optional;
21 import java.util.logging.Logger;
22 import software.amazon.smithy.codegen.core.CodegenException;
23 import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider;
24 import software.amazon.smithy.codegen.core.ReservedWords;
25 import software.amazon.smithy.codegen.core.ReservedWordsBuilder;
26 import software.amazon.smithy.codegen.core.Symbol;
27 import software.amazon.smithy.codegen.core.SymbolProvider;
28 import software.amazon.smithy.go.codegen.knowledge.GoPointableIndex;
29 import software.amazon.smithy.go.codegen.trait.UnexportedMemberTrait;
30 import software.amazon.smithy.model.Model;
31 import software.amazon.smithy.model.knowledge.NeighborProviderIndex;
32 import software.amazon.smithy.model.neighbor.NeighborProvider;
33 import software.amazon.smithy.model.neighbor.Relationship;
34 import software.amazon.smithy.model.neighbor.RelationshipType;
35 import software.amazon.smithy.model.shapes.BigDecimalShape;
36 import software.amazon.smithy.model.shapes.BigIntegerShape;
37 import software.amazon.smithy.model.shapes.BlobShape;
38 import software.amazon.smithy.model.shapes.BooleanShape;
39 import software.amazon.smithy.model.shapes.ByteShape;
40 import software.amazon.smithy.model.shapes.CollectionShape;
41 import software.amazon.smithy.model.shapes.DocumentShape;
42 import software.amazon.smithy.model.shapes.DoubleShape;
43 import software.amazon.smithy.model.shapes.FloatShape;
44 import software.amazon.smithy.model.shapes.IntegerShape;
45 import software.amazon.smithy.model.shapes.ListShape;
46 import software.amazon.smithy.model.shapes.LongShape;
47 import software.amazon.smithy.model.shapes.MapShape;
48 import software.amazon.smithy.model.shapes.MemberShape;
49 import software.amazon.smithy.model.shapes.OperationShape;
50 import software.amazon.smithy.model.shapes.ResourceShape;
51 import software.amazon.smithy.model.shapes.ServiceShape;
52 import software.amazon.smithy.model.shapes.SetShape;
53 import software.amazon.smithy.model.shapes.Shape;
54 import software.amazon.smithy.model.shapes.ShapeId;
55 import software.amazon.smithy.model.shapes.ShapeVisitor;
56 import software.amazon.smithy.model.shapes.ShortShape;
57 import software.amazon.smithy.model.shapes.StringShape;
58 import software.amazon.smithy.model.shapes.StructureShape;
59 import software.amazon.smithy.model.shapes.TimestampShape;
60 import software.amazon.smithy.model.shapes.UnionShape;
61 import software.amazon.smithy.model.traits.EnumTrait;
62 import software.amazon.smithy.model.traits.ErrorTrait;
63 import software.amazon.smithy.model.traits.StreamingTrait;
64 import software.amazon.smithy.utils.StringUtils;
65 
66 /**
67  * Responsible for type mapping and file/identifier formatting.
68  *
69  * <p>Reserved words for Go are automatically escaped so that they are
70  * suffixed with "_". See "reserved-words.txt" for the list of words.
71  */
72 final class SymbolVisitor implements SymbolProvider, ShapeVisitor<Symbol> {
73     private static final Logger LOGGER = Logger.getLogger(SymbolVisitor.class.getName());
74 
75     private final Model model;
76     private final String rootModuleName;
77     private final String typesPackageName;
78     private final ReservedWordSymbolProvider.Escaper escaper;
79     private final ReservedWordSymbolProvider.Escaper errorMemberEscaper;
80     private final Map<ShapeId, ReservedWordSymbolProvider.Escaper> structureSpecificMemberEscapers = new HashMap<>();
81     private final GoPointableIndex pointableIndex;
82 
83 
SymbolVisitor(Model model, String rootModuleName)84     SymbolVisitor(Model model, String rootModuleName) {
85         this.model = model;
86         this.rootModuleName = rootModuleName;
87         this.typesPackageName = rootModuleName + "/types";
88         this.pointableIndex = GoPointableIndex.of(model);
89 
90         // Reserve the generated names for union members, including the unknown case.
91         ReservedWordsBuilder reservedNames = new ReservedWordsBuilder()
92                 .put(UnionGenerator.UNKNOWN_MEMBER_NAME,
93                         escapeWithTrailingUnderscore(UnionGenerator.UNKNOWN_MEMBER_NAME));
94         reserveUnionMemberNames(model, reservedNames);
95 
96         ReservedWords reservedMembers = new ReservedWordsBuilder()
97                 // Since Go only exports names if the first character is upper case and all
98                 // the go reserved words are lower case, it's functionally impossible to conflict,
99                 // so we only need to protect against common names. As of now there's only one.
100                 .put("String", "String_")
101                 .build();
102 
103         model.shapes(StructureShape.class)
104                 .filter(this::supportsInheritance)
105                 .forEach(this::reserveInterfaceMemberAccessors);
106 
107         escaper = ReservedWordSymbolProvider.builder()
108                 .nameReservedWords(reservedNames.build())
109                 .memberReservedWords(reservedMembers)
110                 // Only escape words when the symbol has a definition file to
111                 // prevent escaping intentional references to built-in types.
112                 .escapePredicate((shape, symbol) -> !StringUtils.isEmpty(symbol.getDefinitionFile()))
113                 .buildEscaper();
114 
115         // Reserved words that only apply to error members.
116         ReservedWords reservedErrorMembers = new ReservedWordsBuilder()
117                 .put("ErrorCode", "ErrorCode_")
118                 .put("ErrorFault", "ErrorFault_")
119                 .put("Unwrap", "Unwrap_")
120                 .put("Error", "Error_")
121                 .build();
122 
123         errorMemberEscaper = ReservedWordSymbolProvider.builder()
124                 .memberReservedWords(ReservedWords.compose(reservedMembers, reservedErrorMembers))
125                 .escapePredicate((shape, symbol) -> !StringUtils.isEmpty(symbol.getDefinitionFile()))
126                 .buildEscaper();
127     }
128 
129     /**
130      * Reserves generated member names for unions.
131      *
132      * <p>These have the format {UnionName}Member{MemberName}.
133      *
134      * @param model   The model whose unions should be reserved.
135      * @param builder A reserved words builder to add on to.
136      */
reserveUnionMemberNames(Model model, ReservedWordsBuilder builder)137     private void reserveUnionMemberNames(Model model, ReservedWordsBuilder builder) {
138         model.shapes(UnionShape.class).forEach(union -> {
139             for (MemberShape member : union.getAllMembers().values()) {
140                 String memberName = formatUnionMemberName(union, member);
141                 builder.put(memberName, escapeWithTrailingUnderscore(memberName));
142             }
143         });
144     }
145 
supportsInheritance(Shape shape)146     private boolean supportsInheritance(Shape shape) {
147         return shape.isStructureShape() && shape.hasTrait(ErrorTrait.class);
148     }
149 
150     /**
151      * Reserves Get* and Has* member names for the given structure for use as accessor methods.
152      *
153      * <p>These reservations will only apply to the given structure, not to other structures.
154      *
155      * @param shape The structure shape whose members should be reserved.
156      */
reserveInterfaceMemberAccessors(StructureShape shape)157     private void reserveInterfaceMemberAccessors(StructureShape shape) {
158         ReservedWordsBuilder builder = new ReservedWordsBuilder();
159         for (MemberShape member : shape.getAllMembers().values()) {
160             String name = getDefaultMemberName(member);
161             String getterName = "Get" + name;
162             String haserName = "Has" + name;
163             builder.put(getterName, escapeWithTrailingUnderscore(getterName));
164             builder.put(haserName, escapeWithTrailingUnderscore(haserName));
165         }
166         ReservedWordSymbolProvider.Escaper structureSpecificMemberEscaper = ReservedWordSymbolProvider.builder()
167                 .memberReservedWords(builder.build())
168                 .buildEscaper();
169         structureSpecificMemberEscapers.put(shape.getId(), structureSpecificMemberEscaper);
170     }
171 
escapeWithTrailingUnderscore(String symbolName)172     private String escapeWithTrailingUnderscore(String symbolName) {
173         return symbolName + "_";
174     }
175 
176     @Override
toSymbol(Shape shape)177     public Symbol toSymbol(Shape shape) {
178         Symbol symbol = shape.accept(this);
179         LOGGER.fine(() -> String.format("Creating symbol from %s: %s", shape, symbol));
180         return linkArchetypeShape(shape, escaper.escapeSymbol(shape, symbol));
181     }
182 
183     /**
184      * Links the archetype shape id for the symbol.
185      *
186      * @param shape  the model shape
187      * @param symbol the symbol to set the archetype property on
188      * @return the symbol with archetype set if shape is a synthetic clone otherwise the original symbol
189      */
linkArchetypeShape(Shape shape, Symbol symbol)190     private Symbol linkArchetypeShape(Shape shape, Symbol symbol) {
191         return shape.getTrait(SyntheticClone.class)
192                 .map(syntheticClone -> symbol.toBuilder()
193                         .putProperty("archetype", syntheticClone.getArchetype())
194                         .build())
195                 .orElse(symbol);
196     }
197 
198     @Override
toMemberName(MemberShape shape)199     public String toMemberName(MemberShape shape) {
200         Shape container = model.expectShape(shape.getContainer());
201         if (container.isUnionShape()) {
202             // Union member names are not escaped as they are used to build the escape set.
203             return formatUnionMemberName(container.asUnionShape().get(), shape);
204         }
205 
206         String memberName = getDefaultMemberName(shape);
207         memberName = escaper.escapeMemberName(memberName);
208 
209         // Escape words reserved for the specific container.
210         if (structureSpecificMemberEscapers.containsKey(shape.getContainer())) {
211             memberName = structureSpecificMemberEscapers.get(shape.getContainer()).escapeMemberName(memberName);
212         }
213 
214         // Escape words that are only reserved for error members.
215         if (isErrorMember(shape)) {
216             memberName = errorMemberEscaper.escapeMemberName(memberName);
217         }
218         return memberName;
219     }
220 
formatUnionMemberName(UnionShape union, MemberShape member)221     private String formatUnionMemberName(UnionShape union, MemberShape member) {
222         return String.format("%sMember%s", getDefaultShapeName(union), getDefaultMemberName(member));
223     }
224 
getDefaultShapeName(Shape shape)225     private String getDefaultShapeName(Shape shape) {
226         return StringUtils.capitalize(removeLeadingInvalidIdentCharacters(shape.getId().getName()));
227     }
228 
getDefaultMemberName(MemberShape shape)229     private String getDefaultMemberName(MemberShape shape) {
230         String memberName = StringUtils.capitalize(removeLeadingInvalidIdentCharacters(shape.getMemberName()));
231 
232         // change to lowercase first character if unexported structure member.
233         if (model.expectShape(shape.getContainer()).isStructureShape() && shape.hasTrait(UnexportedMemberTrait.class)) {
234             memberName = Character.toLowerCase(memberName.charAt(0)) + memberName.substring(1);
235         }
236 
237         return memberName;
238     }
239 
removeLeadingInvalidIdentCharacters(String value)240     private String removeLeadingInvalidIdentCharacters(String value) {
241         if (Character.isAlphabetic(value.charAt(0))) {
242             return value;
243         }
244 
245         int i;
246         for (i = 0; i < value.length(); i++) {
247             if (Character.isAlphabetic(value.charAt(i))) {
248                 break;
249             }
250         }
251 
252         String remaining = value.substring(i);
253         if (remaining.length() == 0) {
254             throw new CodegenException("tried to clean name " + value + ", but resulted in empty string");
255         }
256 
257         return remaining;
258     }
259 
260 
isErrorMember(MemberShape shape)261     private boolean isErrorMember(MemberShape shape) {
262         return model.getShape(shape.getContainer())
263                 .map(container -> container.hasTrait(ErrorTrait.ID))
264                 .orElse(false);
265     }
266 
267     @Override
blobShape(BlobShape shape)268     public Symbol blobShape(BlobShape shape) {
269         if (shape.hasTrait(StreamingTrait.ID)) {
270             Symbol inputVariant = symbolBuilderFor(shape, "Reader", SmithyGoDependency.IO).build();
271             return symbolBuilderFor(shape, "ReadCloser", SmithyGoDependency.IO)
272                     .putProperty(SymbolUtils.INPUT_VARIANT, inputVariant)
273                     .build();
274         }
275         return symbolBuilderFor(shape, "[]byte")
276                 .putProperty(SymbolUtils.GO_UNIVERSE_TYPE, true)
277                 .build();
278     }
279 
280     @Override
booleanShape(BooleanShape shape)281     public Symbol booleanShape(BooleanShape shape) {
282         return symbolBuilderFor(shape, "bool")
283                 .putProperty(SymbolUtils.GO_UNIVERSE_TYPE, true)
284                 .build();
285     }
286 
287     @Override
listShape(ListShape shape)288     public Symbol listShape(ListShape shape) {
289         return createCollectionSymbol(shape);
290     }
291 
292     @Override
setShape(SetShape shape)293     public Symbol setShape(SetShape shape) {
294         // Go doesn't have a set type. Rather than hack together a set using a map,
295         // we instead just create a list and let the service be responsible for
296         // asserting that there are no duplicates.
297         return createCollectionSymbol(shape);
298     }
299 
createCollectionSymbol(CollectionShape shape)300     private Symbol createCollectionSymbol(CollectionShape shape) {
301         Symbol reference = toSymbol(shape.getMember());
302         // Shape name will be unused for symbols that represent a slice, but in the event it does we set the collection
303         // shape's name to make debugging simpler.
304         return symbolBuilderFor(shape, getDefaultShapeName(shape))
305                 .putProperty(SymbolUtils.GO_SLICE, true)
306                 .putProperty(SymbolUtils.GO_UNIVERSE_TYPE,
307                         reference.getProperty(SymbolUtils.GO_UNIVERSE_TYPE, Boolean.class).orElse(false))
308                 .putProperty(SymbolUtils.GO_ELEMENT_TYPE, reference)
309                 .build();
310     }
311 
312     @Override
mapShape(MapShape shape)313     public Symbol mapShape(MapShape shape) {
314         Symbol reference = toSymbol(shape.getValue());
315         // Shape name will be unused for symbols that represent a map, but in the event it does we set the map shape's
316         // name to make debugging simpler.
317         return symbolBuilderFor(shape, getDefaultShapeName(shape))
318                 .putProperty(SymbolUtils.GO_MAP, true)
319                 .putProperty(SymbolUtils.GO_UNIVERSE_TYPE,
320                         reference.getProperty(SymbolUtils.GO_UNIVERSE_TYPE, Boolean.class).orElse(false))
321                 .putProperty(SymbolUtils.GO_ELEMENT_TYPE, reference)
322                 .build();
323     }
324 
symbolBuilderFor(Shape shape, String typeName)325     private Symbol.Builder symbolBuilderFor(Shape shape, String typeName) {
326         if (pointableIndex.isPointable(shape)) {
327             return SymbolUtils.createPointableSymbolBuilder(shape, typeName);
328         }
329 
330         return SymbolUtils.createValueSymbolBuilder(shape, typeName);
331     }
332 
symbolBuilderFor(Shape shape, String typeName, GoDependency namespace)333     private Symbol.Builder symbolBuilderFor(Shape shape, String typeName, GoDependency namespace) {
334         if (pointableIndex.isPointable(shape)) {
335             return SymbolUtils.createPointableSymbolBuilder(shape, typeName, namespace);
336         }
337 
338         return SymbolUtils.createValueSymbolBuilder(shape, typeName, namespace);
339     }
340 
symbolBuilderFor(Shape shape, String typeName, String namespace)341     private Symbol.Builder symbolBuilderFor(Shape shape, String typeName, String namespace) {
342         if (pointableIndex.isPointable(shape)) {
343             return SymbolUtils.createPointableSymbolBuilder(shape, typeName, namespace);
344         }
345 
346         return SymbolUtils.createValueSymbolBuilder(shape, typeName, namespace);
347     }
348 
349     @Override
byteShape(ByteShape shape)350     public Symbol byteShape(ByteShape shape) {
351         return symbolBuilderFor(shape, "int8")
352                 .putProperty(SymbolUtils.GO_UNIVERSE_TYPE, true)
353                 .build();
354     }
355 
356     @Override
shortShape(ShortShape shape)357     public Symbol shortShape(ShortShape shape) {
358         return symbolBuilderFor(shape, "int16")
359                 .putProperty(SymbolUtils.GO_UNIVERSE_TYPE, true)
360                 .build();
361     }
362 
363     @Override
integerShape(IntegerShape shape)364     public Symbol integerShape(IntegerShape shape) {
365         return symbolBuilderFor(shape, "int32")
366                 .putProperty(SymbolUtils.GO_UNIVERSE_TYPE, true)
367                 .build();
368     }
369 
370     @Override
longShape(LongShape shape)371     public Symbol longShape(LongShape shape) {
372         return symbolBuilderFor(shape, "int64")
373                 .putProperty(SymbolUtils.GO_UNIVERSE_TYPE, true)
374                 .build();
375     }
376 
377     @Override
floatShape(FloatShape shape)378     public Symbol floatShape(FloatShape shape) {
379         return symbolBuilderFor(shape, "float32")
380                 .putProperty(SymbolUtils.GO_UNIVERSE_TYPE, true)
381                 .build();
382     }
383 
384     @Override
doubleShape(DoubleShape shape)385     public Symbol doubleShape(DoubleShape shape) {
386         return symbolBuilderFor(shape, "float64")
387                 .putProperty(SymbolUtils.GO_UNIVERSE_TYPE, true)
388                 .build();
389     }
390 
391     @Override
bigIntegerShape(BigIntegerShape shape)392     public Symbol bigIntegerShape(BigIntegerShape shape) {
393         return createBigSymbol(shape, "Int");
394     }
395 
396     @Override
bigDecimalShape(BigDecimalShape shape)397     public Symbol bigDecimalShape(BigDecimalShape shape) {
398 
399         return createBigSymbol(shape, "Float");
400     }
401 
createBigSymbol(Shape shape, String symbolName)402     private Symbol createBigSymbol(Shape shape, String symbolName) {
403         return symbolBuilderFor(shape, symbolName, SmithyGoDependency.BIG)
404                 .build();
405     }
406 
407     @Override
documentShape(DocumentShape shape)408     public Symbol documentShape(DocumentShape shape) {
409         return symbolBuilderFor(shape, "Document", SmithyGoDependency.SMITHY)
410                 .build();
411     }
412 
413     @Override
operationShape(OperationShape shape)414     public Symbol operationShape(OperationShape shape) {
415         String name = getDefaultShapeName(shape);
416         return SymbolUtils.createPointableSymbolBuilder(shape, name, rootModuleName)
417                 .definitionFile(String.format("./api_op_%s.go", name))
418                 .build();
419     }
420 
421     @Override
resourceShape(ResourceShape shape)422     public Symbol resourceShape(ResourceShape shape) {
423         // TODO: implement resources
424         return SymbolUtils.createPointableSymbolBuilder(shape, "nil").build();
425     }
426 
427     @Override
serviceShape(ServiceShape shape)428     public Symbol serviceShape(ServiceShape shape) {
429         return symbolBuilderFor(shape, "Client", rootModuleName)
430                 .definitionFile("./api_client.go")
431                 .build();
432     }
433 
434     @Override
stringShape(StringShape shape)435     public Symbol stringShape(StringShape shape) {
436         if (shape.hasTrait(EnumTrait.class)) {
437             String name = getDefaultShapeName(shape);
438             return symbolBuilderFor(shape, name, typesPackageName)
439                     .definitionFile("./types/enums.go")
440                     .build();
441         }
442 
443         return symbolBuilderFor(shape, "string")
444                 .putProperty(SymbolUtils.GO_UNIVERSE_TYPE, true)
445                 .build();
446     }
447 
448     @Override
structureShape(StructureShape shape)449     public Symbol structureShape(StructureShape shape) {
450         String name = getDefaultShapeName(shape);
451         if (shape.getId().getNamespace().equals(CodegenUtils.getSyntheticTypeNamespace())) {
452             Optional<String> boundOperationName = getNameOfBoundOperation(shape);
453             if (boundOperationName.isPresent()) {
454                 return symbolBuilderFor(shape, name, rootModuleName)
455                         .definitionFile("./api_op_" + boundOperationName.get() + ".go")
456                         .build();
457             }
458         }
459 
460         Symbol.Builder builder = symbolBuilderFor(shape, name, typesPackageName);
461         if (shape.hasTrait(ErrorTrait.ID)) {
462             builder.definitionFile("./types/errors.go");
463         } else {
464             builder.definitionFile("./types/types.go");
465         }
466 
467         return builder.build();
468     }
469 
getNameOfBoundOperation(StructureShape shape)470     private Optional<String> getNameOfBoundOperation(StructureShape shape) {
471         NeighborProvider provider = NeighborProviderIndex.of(model).getReverseProvider();
472         for (Relationship relationship : provider.getNeighbors(shape)) {
473             RelationshipType relationshipType = relationship.getRelationshipType();
474             if (relationshipType == RelationshipType.INPUT || relationshipType == RelationshipType.OUTPUT) {
475                 return Optional.of(getDefaultShapeName(relationship.getNeighborShape().get()));
476             }
477         }
478         return Optional.empty();
479     }
480 
481     @Override
unionShape(UnionShape shape)482     public Symbol unionShape(UnionShape shape) {
483         String name = getDefaultShapeName(shape);
484         return symbolBuilderFor(shape, name, typesPackageName)
485                 .definitionFile("./types/types.go")
486                 .build();
487     }
488 
489     @Override
memberShape(MemberShape member)490     public Symbol memberShape(MemberShape member) {
491         Shape targetShape = model.expectShape(member.getTarget());
492         return toSymbol(targetShape)
493                 .toBuilder()
494                 .putProperty(SymbolUtils.POINTABLE, pointableIndex.isPointable(member))
495                 .build();
496     }
497 
498     @Override
timestampShape(TimestampShape shape)499     public Symbol timestampShape(TimestampShape shape) {
500         return symbolBuilderFor(shape, "Time", SmithyGoDependency.TIME).build();
501     }
502 }
503