Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-6933447.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "AWS SDK for Java v2",
"contributor": "",
"description": "Add the `@SdkAdvancedApi` annotation, which marks APIs that are error-prone to implement, override, call, or configure so that using them incorrectly compiles cleanly but can fail or misbehave at runtime. The annotation records structured guidance (the risky usage kind, an explanation of the contract to uphold, a safer alternative, and a documentation link) and is applied to several streaming and interceptor extension points, including AsyncRequestBody, AsyncResponseTransformer, ContentStreamProvider, the mutating ExecutionInterceptor content hooks, and the FUTURE_COMPLETION_EXECUTOR advanced client option."
}
12 changes: 12 additions & 0 deletions build-tools/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@
<skip>true</skip>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>

</build>
Expand All @@ -74,6 +80,12 @@
<artifactId>spotbugs</artifactId>
<version>4.8.6</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.3</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.buildtools.checkstyle;

import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FullIdent;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import java.util.Arrays;
import java.util.List;

/**
* Caps the length of the {@code guidance} and {@code saferAlternative} member values on any
* {@code @SdkAdvancedApi} usage. The values are written as multi-line string-literal concatenations, so this operates
* on the source AST and sums the decoded content of every concatenated string literal in the member's expression before
* comparing to {@code max}.
*/
public class SdkAdvancedApiMemberLengthCheck extends AbstractCheck {

private static final String ANNOTATION_NAME = "SdkAdvancedApi";
private static final List<String> CHECKED_MEMBERS = Arrays.asList("guidance", "saferAlternative");

private int max = 1000;

public void setMax(int max) {
this.max = max;
}

@Override
public int[] getDefaultTokens() {
return getRequiredTokens();
}

@Override
public int[] getAcceptableTokens() {
return getRequiredTokens();
}

@Override
public int[] getRequiredTokens() {
return new int[] {TokenTypes.ANNOTATION};
}

@Override
public void visitToken(DetailAST annotation) {
if (!isSdkAdvancedApi(annotation)) {
return;
}

for (DetailAST child = annotation.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child.getType() != TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
continue;
}

DetailAST memberName = child.findFirstToken(TokenTypes.IDENT);
if (memberName == null || !CHECKED_MEMBERS.contains(memberName.getText())) {
continue;
}

DetailAST expr = child.findFirstToken(TokenTypes.EXPR);
if (expr == null) {
continue;
}

int length = concatenatedStringLength(expr);
if (length > max) {
log(memberName, String.format(
"@%s member '%s' is %d characters, which exceeds the maximum of %d. Tighten the wording.",
ANNOTATION_NAME, memberName.getText(), length, max));
}
}
}

private boolean isSdkAdvancedApi(DetailAST annotation) {
DetailAST nameNode = annotation.findFirstToken(TokenTypes.IDENT);
if (nameNode == null) {
nameNode = annotation.findFirstToken(TokenTypes.DOT);
}
if (nameNode == null) {
return false;
}
String name = FullIdent.createFullIdent(nameNode).getText();
return name.equals(ANNOTATION_NAME) || name.endsWith("." + ANNOTATION_NAME);
}

private int concatenatedStringLength(DetailAST node) {
int total = 0;
for (DetailAST child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child.getType() == TokenTypes.STRING_LITERAL) {
total += decodedLength(child.getText());
} else {
total += concatenatedStringLength(child);
}
}
return total;
}

/**
* Counts the logical characters in a string literal token (its text includes the surrounding quotes), treating each
* escape sequence as a single character so the count matches the text a consumer of the annotation would see.
*/
private int decodedLength(String literalWithQuotes) {
String body = literalWithQuotes;
if (body.length() >= 2 && body.charAt(0) == '"' && body.charAt(body.length() - 1) == '"') {
body = body.substring(1, body.length() - 1);
}

int count = 0;
int i = 0;
while (i < body.length()) {
if (body.charAt(i) == '\\' && i + 1 < body.length()) {
char next = body.charAt(i + 1);
if (next == 'u') {
i += 6;
} else if (next >= '0' && next <= '7') {
int j = i + 1;
int digits = 0;
while (j < body.length() && digits < 3 && body.charAt(j) >= '0' && body.charAt(j) <= '7') {
j++;
digits++;
}
i = j;
} else {
i += 2;
}
} else {
i++;
}
count++;
}
return count;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,11 @@
<!-- Checks that every class is marked with Sdk Annotation -->
<module name="software.amazon.awssdk.buildtools.checkstyle.MissingSdkAnnotationCheck" />

<!-- Caps the length of @SdkAdvancedApi guidance and saferAlternative member values -->
<module name="software.amazon.awssdk.buildtools.checkstyle.SdkAdvancedApiMemberLengthCheck">
<property name="max" value="1000"/>
</module>

<!-- Checks that no 'final' are added on local variables -->
<module name="software.amazon.awssdk.buildtools.checkstyle.UnnecessaryFinalOnLocalVariableCheck" />

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.buildtools.checkstyle;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.puppycrawl.tools.checkstyle.Checker;
import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.AuditListener;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

class SdkAdvancedApiMemberLengthCheckTest {

@TempDir
Path tempDir;

@Test
void guidanceWithinLimit_passes() throws Exception {
String source = classWithMembers(repeat("a", 50), repeat("b", 50));
assertEquals(Collections.emptyList(), runCheck(source, 1000));
}

@Test
void guidanceOverLimit_fails() throws Exception {
String source = classWithMembers(repeat("a", 1001), repeat("b", 50));
List<String> violations = runCheck(source, 1000);
assertEquals(1, violations.size());
assertTrue(violations.get(0).contains("guidance"), violations.get(0));
assertTrue(violations.get(0).contains("1001"), violations.get(0));
}

@Test
void saferAlternativeOverLimit_fails() throws Exception {
String source = classWithMembers(repeat("a", 50), repeat("b", 1001));
List<String> violations = runCheck(source, 1000);
assertEquals(1, violations.size());
assertTrue(violations.get(0).contains("saferAlternative"), violations.get(0));
}

@Test
void multiLineConcatenation_sumsAllFragments() throws Exception {
String guidance = "\"" + repeat("a", 400) + "\"\n"
+ " + \"" + repeat("b", 400) + "\"\n"
+ " + \"" + repeat("c", 300) + "\"";
String source = classWithRawGuidance(guidance);
List<String> violations = runCheck(source, 1000);
assertEquals(1, violations.size());
assertTrue(violations.get(0).contains("1100"), violations.get(0));
}

@Test
void valueAtExactLimit_passes() throws Exception {
String source = classWithMembers(repeat("a", 1000), repeat("b", 1000));
assertEquals(Collections.emptyList(), runCheck(source, 1000));
}

private static String repeat(String s, int n) {
return IntStream.range(0, n).mapToObj(i -> s).collect(Collectors.joining());
}

private static String classWithMembers(String guidance, String saferAlternative) {
return classWithRawGuidanceAndSafer("\"" + guidance + "\"", "\"" + saferAlternative + "\"");
}

private static String classWithRawGuidance(String rawGuidanceExpr) {
return classWithRawGuidanceAndSafer(rawGuidanceExpr, "\"safe\"");
}

private static String classWithRawGuidanceAndSafer(String rawGuidanceExpr, String rawSaferExpr) {
return "package p;\n"
+ "@SdkAdvancedApi(\n"
+ " cautionWhen = Usage.IMPLEMENTED,\n"
+ " guidance = " + rawGuidanceExpr + ",\n"
+ " saferAlternative = " + rawSaferExpr + ")\n"
+ "public interface Foo {\n"
+ "}\n";
}

private List<String> runCheck(String source, int max) throws IOException, CheckstyleException {
File file = tempDir.resolve("Foo.java").toFile();
Files.write(file.toPath(), source.getBytes(StandardCharsets.UTF_8));

DefaultConfiguration checkConfig = new DefaultConfiguration(SdkAdvancedApiMemberLengthCheck.class.getName());
checkConfig.addAttribute("max", Integer.toString(max));

DefaultConfiguration treeWalker = new DefaultConfiguration("TreeWalker");
treeWalker.addChild(checkConfig);

DefaultConfiguration checker = new DefaultConfiguration("Checker");
checker.addAttribute("charset", "UTF-8");
checker.addChild(treeWalker);

Checker c = new Checker();
c.setModuleClassLoader(Thread.currentThread().getContextClassLoader());
c.configure(checker);

List<String> messages = new ArrayList<>();
c.addListener(new CollectingListener(messages));

c.process(Collections.singletonList(file));
c.destroy();
return messages;
}

private static final class CollectingListener implements AuditListener {
private final List<String> messages;

private CollectingListener(List<String> messages) {
this.messages = messages;
}

@Override
public void addError(AuditEvent event) {
messages.add(event.getMessage());
}

@Override
public void addException(AuditEvent event, Throwable throwable) {
messages.add("EXCEPTION: " + throwable.getMessage());
}

@Override
public void auditStarted(AuditEvent event) {
}

@Override
public void auditFinished(AuditEvent event) {
}

@Override
public void fileStarted(AuditEvent event) {
}

@Override
public void fileFinished(AuditEvent event) {
}
}
}
Loading
Loading