diff --git a/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ServiceClientBuilderCheck.java b/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ServiceClientBuilderCheck.java new file mode 100644 index 000000000000..8623828a9c63 --- /dev/null +++ b/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ServiceClientBuilderCheck.java @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.tools.checkstyle.checks; + +import com.puppycrawl.tools.checkstyle.api.AbstractCheck; +import com.puppycrawl.tools.checkstyle.api.DetailAST; +import com.puppycrawl.tools.checkstyle.api.TokenTypes; + +import java.util.Stack; + +/** + * The @ServiceClientBuilder class should have the following rules: + * 1) All service client builder should be named ClientBuilder and annotated with @ServiceClientBuilder. + * 2) No other method have prefix 'build' other than 'build*Client' or 'build*AsyncClient'. + */ +public class ServiceClientBuilderCheck extends AbstractCheck { + private static final String SERVICE_CLIENT_BUILDER = "ServiceClientBuilder"; + + private Stack hasServiceClientBuilderAnnotationStack = new Stack(); + private Stack hasBuildMethodStack = new Stack<>(); + private boolean hasServiceClientBuilderAnnotation; + private boolean hasBuildMethod; + + @Override + public int[] getDefaultTokens() { + return getRequiredTokens(); + } + + @Override + public int[] getAcceptableTokens() { + return getRequiredTokens(); + } + + @Override + public int[] getRequiredTokens() { + return new int[] { + TokenTypes.CLASS_DEF, + TokenTypes.METHOD_DEF + }; + } + + @Override + public void leaveToken(DetailAST token) { + if (token.getType() == TokenTypes.CLASS_DEF) { + hasServiceClientBuilderAnnotation = hasServiceClientBuilderAnnotationStack.pop(); + hasBuildMethod = hasBuildMethodStack.pop(); + if (hasServiceClientBuilderAnnotation && !hasBuildMethod) { + log(token, "Class with @ServiceClientBuilder annotation must have a method starting with ''build'' and ending with ''Client''."); + } + } + } + + @Override + public void visitToken(DetailAST token) { + switch (token.getType()) { + case TokenTypes.CLASS_DEF: + // Save the state of variable 'hasServiceClientBuilderAnnotation' to limit the scope of accessibility + hasServiceClientBuilderAnnotationStack.push(hasServiceClientBuilderAnnotation); + hasBuildMethodStack.push(hasBuildMethod); + final DetailAST serviceClientAnnotationBuilderToken = getServiceClientBuilderAnnotation(token); + final String className = token.findFirstToken(TokenTypes.IDENT).getText(); + + hasServiceClientBuilderAnnotation = serviceClientAnnotationBuilderToken != null; + if (hasServiceClientBuilderAnnotation) { + // Don't need to check if the 'serviceClients' exist. It is required when using @ServiceClientBuilder + + // HAS @ServiceClientBuilder annotation but NOT named the class ClientBuilder + if (!className.endsWith("ClientBuilder")) { + log(token, String.format("Class annotated with @ServiceClientBuilder ''%s'' should be named ClientBuilder.", className)); + } + } else { + // No @ServiceClientBuilder annotation but HAS named the class ClientBuilder + if (className.endsWith("ClientBuilder")) { + log(token, String.format("Class ''%s'' should be annotated with @ServiceClientBuilder.", className)); + } + } + break; + case TokenTypes.METHOD_DEF: + if (!hasServiceClientBuilderAnnotation) { + return; + } + + final String methodName = token.findFirstToken(TokenTypes.IDENT).getText(); + if (!methodName.startsWith("build")) { + break; + } + + hasBuildMethod = true; + // method name has prefix 'build' but not 'build*Client' or 'build*AsyncClient' + if (!methodName.endsWith("Client")) { + log(token, String.format( + "@ServiceClientBuilder class should not have a method name, ''%s'' starting with ''build'' but not ending with ''Client''." , methodName)); + } + break; + default: + // Checkstyle complains if there's no default block in switch + break; + } + } + + /** + * Checks if the class is annotated with @ServiceClientBuilder. + * + * @param classDefToken the CLASS_DEF AST node + * @return the annotation node if the class is annotated with @ServiceClientBuilder, null otherwise. + */ + private DetailAST getServiceClientBuilderAnnotation(DetailAST classDefToken) { + final DetailAST modifiersToken = classDefToken.findFirstToken(TokenTypes.MODIFIERS); + + if (!modifiersToken.branchContains(TokenTypes.ANNOTATION)) { + return null; + } + + DetailAST annotationToken = modifiersToken.findFirstToken(TokenTypes.ANNOTATION); + if (!SERVICE_CLIENT_BUILDER.equals(annotationToken.findFirstToken(TokenTypes.IDENT).getText())) { + return null; + } + + return annotationToken; + } +} diff --git a/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ServiceInterfaceCheck.java b/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ServiceInterfaceCheck.java new file mode 100644 index 000000000000..3ac00ff0cd08 --- /dev/null +++ b/eng/code-quality-reports/src/main/java/com/azure/tools/checkstyle/checks/ServiceInterfaceCheck.java @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.tools.checkstyle.checks; + +import com.puppycrawl.tools.checkstyle.api.AbstractCheck; +import com.puppycrawl.tools.checkstyle.api.DetailAST; +import com.puppycrawl.tools.checkstyle.api.TokenTypes; + +import java.util.regex.Pattern; + +/** + * The @ServiceInterface class should have the following rules: + * 1) The annotation property 'name' should be non-empty + * 2) The length of value of property 'name' should be less than 20 characters and without space + */ +public class ServiceInterfaceCheck extends AbstractCheck { + @Override + public int[] getDefaultTokens() { + return getRequiredTokens(); + } + + @Override + public int[] getAcceptableTokens() { + return getRequiredTokens(); + } + + @Override + public int[] getRequiredTokens() { + return new int[] { + TokenTypes.INTERFACE_DEF + }; + } + + @Override + public void visitToken(DetailAST token) { + if (token.getType() == TokenTypes.INTERFACE_DEF) { + checkServiceInterface(token); + } + } + + /** + * The @ServiceInterface class should have the following rules: + * 1) The annotation property 'name' should be non-empty + * 2) The length of value of property 'name' should be less than 20 characters and without space + * + * @param interfaceDefToken INTERFACE_DEF AST node + */ + private void checkServiceInterface(DetailAST interfaceDefToken) { + DetailAST serviceInterfaceAnnotationNode = null; + String nameValue = null; + + DetailAST modifiersToken = interfaceDefToken.findFirstToken(TokenTypes.MODIFIERS); + // Find the @ServiceInterface and the property 'name' and the corresponding value + for (DetailAST ast = modifiersToken.getFirstChild(); ast != null; ast = ast.getNextSibling()) { + // We care about only the ANNOTATION type + if (ast.getType() != TokenTypes.ANNOTATION) { + continue; + } + // Skip if not @ServiceInterface annotation + if (!"ServiceInterface".equals(ast.findFirstToken(TokenTypes.IDENT).getText())) { + continue; + } + + // Get the @ServiceInterface annotation node + serviceInterfaceAnnotationNode = ast; + + // Get the 'name' property value of @ServiceInterface + // @ServiceInterface requires 'name' property + DetailAST annotationMemberValuePairToken = ast.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); + if ("name".equals(annotationMemberValuePairToken.findFirstToken(TokenTypes.IDENT).getText())) { + nameValue = getNamePropertyValue(annotationMemberValuePairToken.findFirstToken(TokenTypes.EXPR)); + break; + } + } + + // Checks the rules: + // Skip the check if no @ServiceInterface annotation found + if (serviceInterfaceAnnotationNode == null) { + return; + } + + // 'name' is required at @ServiceInterface + // 'name' should not be empty, no Space allowed and the length should less than or equal to 20 characters + Pattern serviceNamePattern = Pattern.compile("^[a-zA-Z0-9]{1,20}$"); + if (!serviceNamePattern.matcher(nameValue).find()) { + log(serviceInterfaceAnnotationNode, String.format( + "The ''name'' property of @ServiceInterface, ''%s'' should be non-empty, alphanumeric and not more than 10 characters", + nameValue)); + } + } + + /** + * Get the name property value from the EXPR node + * + * @param exprToken EXPR + * @return null if EXPR node doesn't exist or no STRING_LITERAL. Otherwise, returns the value of the property. + */ + private String getNamePropertyValue(DetailAST exprToken) { + if (exprToken == null) { + return null; + } + + final DetailAST nameValueToken = exprToken.findFirstToken(TokenTypes.STRING_LITERAL); + if (nameValueToken == null) { + return null; + } + + String nameValue = nameValueToken.getText(); + + // remove the beginning and ending double quote + return nameValue.replaceAll("^\"|\"$", ""); + } +} diff --git a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml index ac7af8e88772..5acafa820b75 100755 --- a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml +++ b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml @@ -89,8 +89,23 @@ + + + + + + + + + + + + + + + diff --git a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle.xml b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle.xml index 0e37ecfac62c..41e4303d4378 100755 --- a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle.xml +++ b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle.xml @@ -49,7 +49,7 @@ page at http://checkstyle.sourceforge.net/config.html --> - + @@ -290,5 +290,22 @@ page at http://checkstyle.sourceforge.net/config.html --> We should only return types, and accept argument types, that are from the com.azure namespace. All other types should have suppression added if required. --> + + + + + + + + + + + +