Skip to content

Commit

Permalink
Adding rule for enforcing final field usage - DoNotMerge until all is…
Browse files Browse the repository at this point in the history
…sues are fixed (#5005)

* Adding rule for enforcing final field usage
  • Loading branch information
vhvb1989 authored Aug 19, 2019
1 parent 37a35dd commit 3aa5160
Show file tree
Hide file tree
Showing 37 changed files with 366 additions and 80 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// 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 com.puppycrawl.tools.checkstyle.utils.TokenUtil;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* Verify the whenever a field is assigned just once in constructor to be final
* Tree traversal will pre-compute and fill 3 private containers:
* nonFinalFields: keep an array of non private fields as tokens (to keep line number)
* assignmentsFromConstructor: Save a set of string for each field name that gets its value assigned in constructor
* assignmentsFromMethods: Save a set of strings for each field name that gets updated in any method
*
* On finish tree, check what non-final fields get a value only in constructor and nowhere else by looking for
* strings inside nonFinalFields AND assignmentsFromConstructor but NOT in assignmentsFromMethods
*/
public class EnforceFinalFieldsCheck extends AbstractCheck {
private static final String ERROR_SUGGESTION = "You should consider making the field final, "
+ "or suppressing the warning.";
private static final String ERROR_MSG = "Field \"%s\" is only assigned in constructor and it is not final. "
+ ERROR_SUGGESTION;
private static final String ERROR_FIELD_ALONE = "Field \"%s\" is not assigned in constructor or methods."
+ ERROR_SUGGESTION;

private List<DetailAST> nonFinalFields;
private Set<String> assignmentsFromConstructor;
private Set<String> assignmentsFromMethods;
private DetailAST scopeParent = null;
private Set<String> currentScopeParameterSet = null;
private String currentClassName = null;

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

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

@Override
public int[] getRequiredTokens() {
return new int[] {
TokenTypes.CLASS_DEF,
TokenTypes.ASSIGN,
TokenTypes.PLUS_ASSIGN,
TokenTypes.BAND_ASSIGN,
TokenTypes.BOR_ASSIGN,
TokenTypes.BSR_ASSIGN,
TokenTypes.BXOR_ASSIGN,
TokenTypes.DIV_ASSIGN,
TokenTypes.MINUS_ASSIGN,
TokenTypes.MOD_ASSIGN,
TokenTypes.SL_ASSIGN,
TokenTypes.SR_ASSIGN,
TokenTypes.STAR_ASSIGN,
TokenTypes.INC,
TokenTypes.POST_INC,
TokenTypes.DEC,
TokenTypes.POST_DEC,
TokenTypes.METHOD_DEF,
TokenTypes.CTOR_DEF,
};
}

@Override
public void beginTree(DetailAST root) {
nonFinalFields = new ArrayList<>();
assignmentsFromConstructor = new HashSet<>();
assignmentsFromMethods = new HashSet<>();
}

@Override
public void visitToken(DetailAST token) {
switch (token.getType()) {
case TokenTypes.CLASS_DEF:
this.currentClassName = token.findFirstToken(TokenTypes.IDENT).getText();
fillClassFieldDefinitions(token);
break;
case TokenTypes.ASSIGN:
case TokenTypes.PLUS_ASSIGN:
case TokenTypes.BAND_ASSIGN:
case TokenTypes.BOR_ASSIGN:
case TokenTypes.BSR_ASSIGN:
case TokenTypes.BXOR_ASSIGN:
case TokenTypes.DIV_ASSIGN:
case TokenTypes.MINUS_ASSIGN:
case TokenTypes.MOD_ASSIGN:
case TokenTypes.SL_ASSIGN:
case TokenTypes.SR_ASSIGN:
case TokenTypes.STAR_ASSIGN:
case TokenTypes.INC:
case TokenTypes.POST_INC:
case TokenTypes.DEC:
case TokenTypes.POST_DEC:
checkAssignation(token);
break;
case TokenTypes.METHOD_DEF:
case TokenTypes.CTOR_DEF:
scopeParent = token;
break;
default:
// Checkstyle complains if there's no default block in switch
break;
}
}

@Override
public void leaveToken(DetailAST token) {
switch (token.getType()) {
case TokenTypes.METHOD_DEF:
case TokenTypes.CTOR_DEF:
scopeParent = null;
currentScopeParameterSet = null;
break;
default:
break;
}
}

@Override
public void finishTree(DetailAST token) {
for (DetailAST field : nonFinalFields) {
final String fieldName = field.findFirstToken(TokenTypes.IDENT).getText();
if (assignmentsFromConstructor.contains(fieldName) && !assignmentsFromMethods.contains(fieldName)) {
log(field, String.format(ERROR_MSG, fieldName));
} else if (field.branchContains(TokenTypes.ASSIGN)
&& !assignmentsFromConstructor.contains(fieldName)
&& !assignmentsFromMethods.contains(fieldName)) {
log(field, String.format(ERROR_FIELD_ALONE, fieldName));
}
}
}

/*
* Get the field token from an assignation token.
* This method handles cases for fields referenced as `this.field` or only `field`
* It will get parameters from the method definition to ignore assignations to those parameters
*/
private DetailAST getAssignedField(final DetailAST assignationToken) {
final Set<String> scopeParentParameterSet = getParameterSet(scopeParent.findFirstToken(
TokenTypes.PARAMETERS));
final DetailAST firstChild = assignationToken.getFirstChild();
final DetailAST assignationWithDot = firstChild.getType() == TokenTypes.DOT ? firstChild : null;

if (assignationWithDot != null) {
if (assignationWithDot.branchContains(TokenTypes.LITERAL_THIS)) {
return assignationWithDot.findFirstToken(TokenTypes.IDENT);
} else if (TokenUtil.findFirstTokenByPredicate(assignationWithDot,
token -> token.getText().equals(this.currentClassName)).isPresent()) {
// Case when referencing same class for private static fields
return assignationWithDot.getLastChild();
}
} else {
final DetailAST variableNameToken = assignationToken.getFirstChild();
// make sure the assignation is not for a method parameter
if (!scopeParentParameterSet.contains(variableNameToken.getText())) {
return variableNameToken;
}
}

return null;
}

/*
* Saves a field name to a container depending on the provided type
*/
private void saveField(final String fieldName, final int scopeParentType) {
if (scopeParentType == TokenTypes.METHOD_DEF) {
assignmentsFromMethods.add(fieldName);
} else if (scopeParentType == TokenTypes.CTOR_DEF) {
assignmentsFromConstructor.add(fieldName);
}
}

/*
* Review an assignation to save fields that gets assigned in constructor or in any method
*
* @param assignationToken an assignation token
*/
private void checkAssignation(final DetailAST assignationToken) {
if (scopeParent == null || assignationToken.getChildCount() == 0) {
// not inside any method or constructor definition. No need to check anything
// or this is an assignation from a notation like @Test(timeout = 5000) where assignation has not ChildCount
return;
}

final DetailAST assignationParent = assignationToken.getParent();
if (assignationParent != null && TokenTypes.VARIABLE_DEF == assignationParent.getType()) {
// Assignation for a variable definition. No need to check this assignation
return;
}

DetailAST fieldToken = getAssignedField(assignationToken);

if (fieldToken != null) {
saveField(fieldToken.getText(), scopeParent.getType());
}
}


/*
* Check each non-final field definition from a class and fill nonFinalFields
*
* @param classDefinitionAST a class definition AST
*/
private void fillClassFieldDefinitions(DetailAST classDefinitionAST) {
final DetailAST classObjBlockAst = classDefinitionAST.findFirstToken(TokenTypes.OBJBLOCK);

TokenUtil.forEachChild(classObjBlockAst, TokenTypes.VARIABLE_DEF, (definitionToken) -> {
final DetailAST variableModifiersAst = definitionToken.findFirstToken(TokenTypes.MODIFIERS);
if (!variableModifiersAst.branchContains(TokenTypes.FINAL)
&& !Utils.hasIllegalCombination(variableModifiersAst)) {
nonFinalFields.add(definitionToken);
}
});
}

/*
* Get a node AST with parameters definition and return the list of all parameter names
* The set of parameters is created the first time an assignation is check within a method or constructor
* and we don't need to generate it again until visiting a different method or constructor.
* Field `currentScopeParameterSet` ensures we don't create the set multiple times for the same method/constructor
*
* @param parametersAST a TokenTypes.PARAMETERS
* @return a set of parameter names
*/
private Set<String> getParameterSet(DetailAST parametersAST) {
if (currentScopeParameterSet != null) {
return currentScopeParameterSet;
}
currentScopeParameterSet = new HashSet<>();
TokenUtil.forEachChild(parametersAST, TokenTypes.PARAMETER_DEF, (paramDefToken) -> {
final String parameterName = paramDefToken.findFirstToken(TokenTypes.IDENT).getText();
currentScopeParameterSet.add(parameterName);
});

return currentScopeParameterSet;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@
* Common utils amount custom checks
*/
public class Utils {
/*
* Set of modifiers that cannot be combined with final because it causes a violation.
*/
private static final Set INVALID_FINAL_COMBINATION = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
TokenTypes.LITERAL_TRANSIENT,
TokenTypes.LITERAL_VOLATILE
TokenTypes.LITERAL_VOLATILE,
TokenTypes.LITERAL_DEFAULT,
TokenTypes.LITERAL_PROTECTED
)));

/*
* Set of annotations that cannot be combined with modifier 'final' because it would break serialization.
*/
private static final Set INVALID_FINAL_ANNOTATIONS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
"JsonProperty"
"JsonProperty",
"JsonAlias",
"JacksonXmlProperty"
)));

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,26 @@
<!-- Custom checkstyle rules only check track 2 libraries -->
<suppress checks="com\.azure\.tools\.checkstyle\.checks\..+" files=".*[/\\]com[/\\]microsoft[/\\].*"/>

<!-- Custom checkstyle rules that don't apply to files under test package -->
<!-- Remove this after fixing: https://github.com/Azure/azure-sdk-for-java/issues/5030 -->
<suppress checks="com.azure.tools.checkstyle.checks.EnforceFinalFieldsCheck" files=".*CredentialBuilderBase.java"/>
<suppress checks="com.azure.tools.checkstyle.checks.EnforceFinalFieldsCheck" files=".*KeyCreateOptions.java"/>
<suppress checks="com.azure.tools.checkstyle.checks.EnforceFinalFieldsCheck" files=".*LocalKeyCryptographyClient.java"/>

<!-- Remove this after fixing: https://github.com/Azure/azure-sdk-for-java/issues/5021 -->
<suppress checks="com.azure.tools.checkstyle.checks.EnforceFinalFieldsCheck" files=".*\BlobOutputStream.java"/>

<suppress checks="com\.azure\.tools\.checkstyle\.checks\.(ExternalDependencyExposedCheck|NoImplInPublicAPI|ServiceClientInstantiationCheck
|ServiceClientBuilderCheck|ServiceInterfaceCheck|HttpPipelinePolicyCheck|JavaDocFormatting|JavadocThrowsChecks)" files=".*[/\\]src[/\\]test[/\\]java[/\\].*\.java"/>
|ServiceClientBuilderCheck|ServiceInterfaceCheck|HttpPipelinePolicyCheck|JavaDocFormatting|JavadocThrowsChecks
|EnforceFinalFieldsCheck)" files=".*[/\\]src[/\\]test[/\\]java[/\\].*\.java"/>

<!-- Custom checkstyle rules that don't apply to files under implementation package -->
<suppress checks="com\.azure\.tools\.checkstyle\.checks\.(ExternalDependencyExposedCheck|ServiceClientInstantiationCheck
|ServiceClientBuilderCheck|ServiceInterfaceCheck|JavaDocFormatting|JavadocThrowsChecks)" files=".*[/\\]implementation[/\\].*\.java"/>
|ServiceClientBuilderCheck|ServiceInterfaceCheck|JavaDocFormatting|JavadocThrowsChecks|EnforceFinalFieldsCheck)"
files=".*[/\\]implementation[/\\].*\.java"/>

<!-- Custom checkstyle rules that don't apply to files under samples package -->
<suppress checks="com\.azure\.tools\.checkstyle\.checks\.(ExternalDependencyExposedCheck|HttpPipelinePolicyCheck|JavadocPackage)" files=".*[/\\]samples[/\\].*\.java"/>
<suppress checks="com\.azure\.tools\.checkstyle\.checks\.(ExternalDependencyExposedCheck|HttpPipelinePolicyCheck|JavadocPackage
|EnforceFinalFieldsCheck)" files=".*[/\\]samples[/\\].*\.java"/>

<!-- Don't apply custom Checkstyle rules to files under checkstyle package. -->
<suppress checks="com\.azure\.tools\.checkstyle\.checks\..+" files=".*[/\\]tools[/\\]checkstyle[/\\].*"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,9 @@ page at http://checkstyle.sourceforge.net/config.html -->
<module name="com.azure.tools.checkstyle.checks.OnlyFinalFieldsForImmutableClassCheck"/>

<!-- CUSTOM CHECKS -->
<!-- Verify the whenever a field is assigned just once in constructor to be final -->
<module name="com.azure.tools.checkstyle.checks.EnforceFinalFieldsCheck"/>

<!-- Javadoc format: 'param / return / throws' descriptions text should only have one space character after the
parameter name or return -->
<module name="com.azure.tools.checkstyle.checks.JavaDocFormatting"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public final class ConfigurationClientBuilder {
private HttpClient httpClient;
private HttpLogDetailLevel httpLogDetailLevel;
private HttpPipeline pipeline;
private RetryPolicy retryPolicy;
private final RetryPolicy retryPolicy;
private Configuration configuration;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ private static class CredentialInformation {
private static final String ID = "id=";
private static final String SECRET = "secret=";

private URL baseUri;
private String id;
private byte[] secret;
private final URL baseUri;
private final String id;
private final byte[] secret;

URL baseUri() {
return baseUri;
Expand All @@ -175,24 +175,32 @@ byte[] secret() {
throw new IllegalArgumentException("invalid connection string segment count");
}

URL baseUri = null;
String id = null;
byte[] secret = null;

for (String arg : args) {
String segment = arg.trim();
String lowerCase = segment.toLowerCase(Locale.US);

if (lowerCase.startsWith(ENDPOINT)) {
try {
this.baseUri = new URL(segment.substring(ENDPOINT.length()));
baseUri = new URL(segment.substring(ENDPOINT.length()));
} catch (MalformedURLException ex) {
throw new IllegalArgumentException(ex);
}
} else if (lowerCase.startsWith(ID)) {
this.id = segment.substring(ID.length());
id = segment.substring(ID.length());
} else if (lowerCase.startsWith(SECRET)) {
String secretBase64 = segment.substring(SECRET.length());
this.secret = Base64.getDecoder().decode(secretBase64);
secret = Base64.getDecoder().decode(secretBase64);
}
}

this.baseUri = baseUri;
this.id = id;
this.secret = secret;

if (this.baseUri == null || this.id == null || this.secret == null) {
throw new IllegalArgumentException("Could not parse 'connectionString'."
+ " Expected format: 'endpoint={endpoint};id={id};secret={secret}'. Actual:" + connectionString);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public final class CloudError {
/**
* Details for the error.
*/
private List<CloudError> details;
private final List<CloudError> details;

/**
* Initializes a new instance of CloudError.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/
public abstract class PagedList<E> implements List<E> {
/** The actual items in the list. */
private List<E> items;
private final List<E> items;
/** Stores the latest page fetched. */
private Page<E> currentPage;
/** Cached page right after the current one. */
Expand Down
Loading

0 comments on commit 3aa5160

Please sign in to comment.