Skip to content

Commit 31e72ac

Browse files
authored
fix(codegen): check javascript property validity for property access (#3649)
* fix(codegen): check javascript property validity for property access * fix(codegen): checkstyle fixes * fix(codegen): property access code cleanup
1 parent 71b81a8 commit 31e72ac

File tree

6 files changed

+107
-14
lines changed

6 files changed

+107
-14
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dist-*
4747

4848
*.tsbuildinfo
4949

50+
codegen/.attach*
5051
codegen/.project
5152
codegen/.classpath
5253
codegen/.settings/

codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsRestXml.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
package software.amazon.smithy.aws.typescript.codegen;
1717

18+
import static software.amazon.smithy.aws.typescript.codegen.propertyaccess.PropertyAccessor.getFrom;
19+
1820
import java.util.List;
1921
import java.util.Set;
2022
import software.amazon.smithy.aws.traits.protocols.RestXmlTrait;
@@ -36,6 +38,7 @@
3638
import software.amazon.smithy.typescript.codegen.integration.HttpBindingProtocolGenerator;
3739
import software.amazon.smithy.utils.SmithyInternalApi;
3840

41+
3942
/**
4043
* Handles generating the aws.rest-xml protocol for services. It handles reading and
4144
* writing from document bodies, including generating any functions needed for
@@ -267,10 +270,10 @@ private void serializePayload(
267270
writer.write("let contents: any;");
268271

269272
// Generate an if statement to set the body node if the member is set.
270-
writer.openBlock("if (input.$L !== undefined) {", "}", memberName, () -> {
273+
writer.openBlock("if ($L !== undefined) {", "}", getFrom("input", memberName), () -> {
271274
Shape target = context.getModel().expectShape(member.getTarget());
272275
writer.write("contents = $L;",
273-
getInputValue(context, Location.PAYLOAD, "input." + memberName, member, target));
276+
getInputValue(context, Location.PAYLOAD, getFrom("input", memberName), member, target));
274277

275278
String targetName = target.getTrait(XmlNameTrait.class)
276279
.map(XmlNameTrait::getValue)

codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/DocumentClientCommandGenerator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
package software.amazon.smithy.aws.typescript.codegen;
1717

18+
import static software.amazon.smithy.aws.typescript.codegen.propertyaccess.PropertyAccessor.getFrom;
19+
1820
import java.nio.file.Paths;
1921
import java.util.ArrayList;
2022
import java.util.HashSet;
@@ -176,7 +178,7 @@ private void generateCommandMiddlewareResolver(String configType) {
176178
} else {
177179
writer.openBlock("$L(", ")", marshallInput, () -> {
178180
writer.write("this.input,");
179-
writer.write("this.$L,", COMMAND_INPUT_KEYNODES);
181+
writer.write(getFrom("this", COMMAND_INPUT_KEYNODES) + ",");
180182
writer.write("$L,", marshallOptions);
181183
});
182184
}

codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/JsonShapeDeserVisitor.java

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
package software.amazon.smithy.aws.typescript.codegen;
1717

18+
import static software.amazon.smithy.aws.typescript.codegen.propertyaccess.PropertyAccessor.getFrom;
19+
1820
import java.util.Map;
1921
import java.util.TreeMap;
2022
import java.util.function.BiFunction;
@@ -165,17 +167,22 @@ protected void deserializeStructure(GenerationContext context, StructureShape sh
165167
String locationName = memberNameStrategy.apply(memberShape, memberName);
166168
Shape target = context.getModel().expectShape(memberShape.getTarget());
167169

170+
String propertyAccess = getFrom("output", locationName);
171+
168172
if (usesExpect(target)) {
169173
// Booleans and numbers will call expectBoolean/expectNumber which will handle
170174
// null/undefined properly.
171175
writer.write("$L: $L,",
172176
memberName,
173-
target.accept(getMemberVisitor(memberShape, "output." + locationName)));
177+
target.accept(getMemberVisitor(memberShape, propertyAccess)));
174178
} else {
175-
writer.write("$1L: (output.$2L !== undefined && output.$2L !== null)"
176-
+ " ? $3L: undefined,", memberName, locationName,
177-
// Dispatch to the output value provider for any additional handling.
178-
target.accept(getMemberVisitor(memberShape, "output." + locationName)));
179+
writer.write(
180+
"$1L: ($2L !== undefined && $2L !== null) ? $3L: undefined,",
181+
memberName,
182+
propertyAccess,
183+
// Dispatch to the output value provider for any additional handling.
184+
target.accept(getMemberVisitor(memberShape, propertyAccess))
185+
);
179186
}
180187
});
181188
});
@@ -202,21 +209,25 @@ protected void deserializeUnion(GenerationContext context, UnionShape shape) {
202209
Shape target = model.expectShape(memberShape.getTarget());
203210
String locationName = memberNameStrategy.apply(memberShape, memberName);
204211

205-
String memberValue = target.accept(getMemberVisitor(memberShape, "output." + locationName));
212+
String memberValue = target.accept(getMemberVisitor(memberShape, getFrom("output", locationName)));
206213
if (usesExpect(target)) {
207214
// Booleans and numbers will call expectBoolean/expectNumber which will handle
208215
// null/undefined properly.
209216
writer.openBlock("if ($L !== undefined) {", "}", memberValue, () -> {
210217
writer.write("return { $L: $L as any }", memberName, memberValue);
211218
});
212219
} else {
213-
writer.openBlock("if (output.$L !== undefined && output.$L !== null) {", "}", locationName,
214-
locationName, () -> {
215-
writer.openBlock("return {", "};", () -> {
220+
writer.openBlock(
221+
"if ($1L !== undefined && $1L !== null) {", "}",
222+
getFrom("output", locationName),
223+
() -> writer.openBlock(
224+
"return {", "};",
225+
() -> {
216226
// Dispatch to the output value provider for any additional handling.
217227
writer.write("$L: $L", memberName, memberValue);
218-
});
219-
});
228+
}
229+
)
230+
);
220231
}
221232
});
222233
// Or write to the unknown member the element in the output.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2022 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.aws.typescript.codegen.propertyaccess;
17+
18+
import java.util.regex.Pattern;
19+
20+
public final class PropertyAccessor {
21+
/**
22+
* Starts with alpha or underscore, and contains only alphanumeric and underscores.
23+
*/
24+
public static final Pattern VALID_JAVASCRIPT_PROPERTY_NAME = Pattern.compile("^(?![0-9])[a-zA-Z0-9$_]+$");
25+
26+
private PropertyAccessor() {}
27+
28+
/**
29+
* @param propertyName - property being accessed.
30+
* @return brackets wrapping the name if it's not a valid JavaScript property name.
31+
*/
32+
public static String getPropertyAccessor(String propertyName) {
33+
if (VALID_JAVASCRIPT_PROPERTY_NAME.matcher(propertyName).matches()) {
34+
return "." + propertyName;
35+
}
36+
if (propertyName.contains("\"")) {
37+
// This doesn't handle cases of the special characters being pre-escaped in the propertyName,
38+
// but that case does not currently need to be addressed.
39+
return "[`" + propertyName + "`]";
40+
}
41+
return "[\"" + propertyName + "\"]";
42+
}
43+
44+
/**
45+
* @param variable - object host.
46+
* @param propertyName - property being accessed.
47+
* @return e.g. someObject.prop or someObject['property name'] or reluctantly someObject[`bad"property"name`].
48+
*/
49+
public static String getFrom(String variable, String propertyName) {
50+
return variable + getPropertyAccessor(propertyName);
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package software.amazon.smithy.aws.typescript.codegen.propertyaccess;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.*;
6+
7+
class PropertyAccessorTest {
8+
@Test
9+
void getFrom() {
10+
assertEquals("output.fileSystemId", PropertyAccessor.getFrom("output", "fileSystemId"));
11+
assertEquals("output.__fileSystemId", PropertyAccessor.getFrom("output", "__fileSystemId"));
12+
}
13+
14+
@Test
15+
void getFromQuoted() {
16+
assertEquals("output[\"0fileSystemId\"]", PropertyAccessor.getFrom("output", "0fileSystemId"));
17+
assertEquals("output[\"file-system-id\"]", PropertyAccessor.getFrom("output", "file-system-id"));
18+
}
19+
20+
@Test
21+
void getFromExtraQuoted() {
22+
assertEquals("output[`file\"system\"id`]", PropertyAccessor.getFrom("output", "file\"system\"id"));
23+
}
24+
}

0 commit comments

Comments
 (0)