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