-
-
Notifications
You must be signed in to change notification settings - Fork 755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extend VarCouldBeVal to include analysis of file- and class-level properties #4424
Merged
Merged
Changes from 2 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
09fc793
First draft of refactoring VarCouldBeVal to scan file- and class-leve…
severn-everett 1ad6f25
Added more tests and fixed failing ones
severn-everett 67b60e5
Addressing PR comments
severn-everett eb27411
Added @Parameter to ignore list for internal Detekt scanning; Fixed D…
severn-everett 0921e80
Merge branch 'master' into extend_VarCouldBeVal
severn-everett 34b5cc2
Addressing PR comment
severn-everett File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,17 +10,30 @@ import io.gitlab.arturbosch.detekt.api.Rule | |
import io.gitlab.arturbosch.detekt.api.Severity | ||
import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault | ||
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution | ||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor | ||
import org.jetbrains.kotlin.lexer.KtTokens | ||
import org.jetbrains.kotlin.psi.KtBinaryExpression | ||
import org.jetbrains.kotlin.psi.KtBlockExpression | ||
import org.jetbrains.kotlin.psi.KtExpression | ||
import org.jetbrains.kotlin.psi.KtFile | ||
import org.jetbrains.kotlin.psi.KtIfExpression | ||
import org.jetbrains.kotlin.psi.KtNameReferenceExpression | ||
import org.jetbrains.kotlin.psi.KtNamedDeclaration | ||
import org.jetbrains.kotlin.psi.KtNamedFunction | ||
import org.jetbrains.kotlin.psi.KtObjectDeclaration | ||
import org.jetbrains.kotlin.psi.KtObjectLiteralExpression | ||
import org.jetbrains.kotlin.psi.KtProperty | ||
import org.jetbrains.kotlin.psi.KtQualifiedExpression | ||
import org.jetbrains.kotlin.psi.KtReturnExpression | ||
import org.jetbrains.kotlin.psi.KtUnaryExpression | ||
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType | ||
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType | ||
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject | ||
import org.jetbrains.kotlin.psi.psiUtil.isObjectLiteral | ||
import org.jetbrains.kotlin.psi.psiUtil.isPrivate | ||
import org.jetbrains.kotlin.psi.psiUtil.lastBlockStatementOrThis | ||
import org.jetbrains.kotlin.resolve.BindingContext | ||
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall | ||
import org.jetbrains.kotlin.util.containingNonLocalDeclaration | ||
|
||
private val unaryAssignmentOperators = setOf(KtTokens.MINUSMINUS, KtTokens.PLUSPLUS) | ||
|
||
|
@@ -55,30 +68,25 @@ class VarCouldBeVal(config: Config = Config.empty) : Rule(config) { | |
Debt.FIVE_MINS | ||
) | ||
|
||
override fun visitNamedFunction(function: KtNamedFunction) { | ||
if (bindingContext == BindingContext.EMPTY || function.isSomehowNested()) { | ||
return | ||
} | ||
|
||
override fun visitKtFile(file: KtFile) { | ||
if (bindingContext == BindingContext.EMPTY) return | ||
super.visitKtFile(file) | ||
val assignmentVisitor = AssignmentVisitor(bindingContext) | ||
function.accept(assignmentVisitor) | ||
file.accept(assignmentVisitor) | ||
|
||
assignmentVisitor.getNonReAssignedDeclarations().forEach { | ||
report(CodeSmell(issue, Entity.from(it), "Variable '${it.nameAsSafeName.identifier}' could be val.")) | ||
} | ||
super.visitNamedFunction(function) | ||
} | ||
|
||
private fun KtNamedFunction.isSomehowNested() = | ||
getStrictParentOfType<KtNamedFunction>() != null | ||
|
||
private class AssignmentVisitor(private val bindingContext: BindingContext) : DetektVisitor() { | ||
|
||
private val declarations = mutableSetOf<KtNamedDeclaration>() | ||
private val declarationCandidates = mutableSetOf<KtNamedDeclaration>() | ||
private val assignments = mutableMapOf<String, MutableSet<KtExpression>>() | ||
private val escapeCandidates = mutableMapOf<DeclarationDescriptor, List<KtProperty>>() | ||
|
||
fun getNonReAssignedDeclarations(): List<KtNamedDeclaration> { | ||
return declarations.filterNot { it.hasAssignments() } | ||
return declarationCandidates.filterNot { it.hasAssignments() } | ||
} | ||
|
||
private fun KtNamedDeclaration.hasAssignments(): Boolean { | ||
|
@@ -91,25 +99,114 @@ class VarCouldBeVal(config: Config = Config.empty) : Rule(config) { | |
} | ||
} | ||
|
||
override fun visitNamedFunction(function: KtNamedFunction) { | ||
// The super() call should be first in the function so that any properties | ||
// declared in potential object literals can be evaluated. | ||
super.visitNamedFunction(function) | ||
evaluateReturnExpression(function.initializer) | ||
} | ||
|
||
override fun visitProperty(property: KtProperty) { | ||
if (property.isVar) { | ||
declarations.add(property) | ||
} | ||
super.visitProperty(property) | ||
if (property.isDeclarationCandidate()) { | ||
declarationCandidates.add(property) | ||
} | ||
|
||
// Check for whether the initializer contains an object literal. | ||
bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, property]?.let { | ||
evaluateAssignmentExpression(it, property.initializer) | ||
} | ||
} | ||
|
||
override fun visitUnaryExpression(expression: KtUnaryExpression) { | ||
super.visitUnaryExpression(expression) | ||
if (expression.operationToken in unaryAssignmentOperators) { | ||
visitAssignment(expression.baseExpression) | ||
} | ||
super.visitUnaryExpression(expression) | ||
} | ||
|
||
override fun visitBinaryExpression(expression: KtBinaryExpression) { | ||
super.visitBinaryExpression(expression) | ||
if (expression.operationToken in KtTokens.ALL_ASSIGNMENTS) { | ||
visitAssignment(expression.left) | ||
|
||
// Check for whether the assignment contains an object literal. | ||
(expression.left as? KtNameReferenceExpression)?.let { | ||
it.getResolvedCall(bindingContext)?.resultingDescriptor | ||
}?.let { | ||
evaluateAssignmentExpression(it, expression.right) | ||
} | ||
} | ||
super.visitBinaryExpression(expression) | ||
} | ||
|
||
override fun visitReturnExpression(expression: KtReturnExpression) { | ||
super.visitReturnExpression(expression) | ||
evaluateReturnExpression(expression.returnedExpression) | ||
} | ||
|
||
private fun evaluateAssignmentExpression( | ||
descriptor: DeclarationDescriptor, | ||
rightExpression: KtExpression? | ||
) { | ||
when (rightExpression) { | ||
is KtObjectLiteralExpression -> { | ||
escapeCandidates[descriptor] = rightExpression.collectDescendantsOfType { | ||
it.isEscapeCandidate() | ||
} | ||
} | ||
is KtIfExpression -> { | ||
evaluateAssignmentExpression(descriptor, rightExpression.then) | ||
evaluateAssignmentExpression(descriptor, rightExpression.`else`) | ||
} | ||
is KtBlockExpression -> { | ||
rightExpression.lastBlockStatementOrThis() | ||
.takeIf { it != rightExpression } | ||
?.let { evaluateAssignmentExpression(descriptor, it) } | ||
} | ||
} | ||
} | ||
|
||
private fun evaluateReturnExpression(returnedExpression: KtExpression?) { | ||
when (returnedExpression) { | ||
is KtObjectLiteralExpression -> { | ||
returnedExpression.collectDescendantsOfType<KtProperty> { | ||
it.isEscapeCandidate() | ||
}.forEach(declarationCandidates::remove) | ||
} | ||
is KtNameReferenceExpression -> { | ||
returnedExpression.getResolvedCall(bindingContext)?.resultingDescriptor?.let { descriptor -> | ||
escapeCandidates[descriptor]?.forEach(declarationCandidates::remove) | ||
} | ||
} | ||
is KtIfExpression -> { | ||
evaluateReturnExpression(returnedExpression.then) | ||
evaluateReturnExpression(returnedExpression.`else`) | ||
} | ||
is KtBlockExpression -> { | ||
returnedExpression.lastBlockStatementOrThis() | ||
.takeIf { it != returnedExpression } | ||
?.let { evaluateReturnExpression(it) } | ||
} | ||
} | ||
} | ||
|
||
private fun KtProperty.isDeclarationCandidate(): Boolean { | ||
return when { | ||
!isVar -> false | ||
isLocal || isPrivate() -> true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be good to follow up with hanlding expect / actual properties. |
||
else -> { | ||
// Check for whether property belongs to an anonymous object | ||
// defined in a function. | ||
containingClassOrObject | ||
?.takeIf { it.isObjectLiteral() } | ||
?.containingNonLocalDeclaration() != null | ||
} | ||
} | ||
} | ||
|
||
private fun KtProperty.isEscapeCandidate(): Boolean { | ||
return !isPrivate() && | ||
(containingClassOrObject as? KtObjectDeclaration)?.isObjectLiteral() == true | ||
} | ||
|
||
private fun visitAssignment(assignedExpression: KtExpression?) { | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's no
bindingContext
available, the super method isn't called.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe that's desirable, right? Faster rule.
I think that we should even move this checks earlier in the life cycle. But that's another topic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, you don't gain any performance benefit, because
KtFile
's are usually not nested.I agree with the following statement. In the best case, you don't even load the rule if it's annotated with the corresponding annotation. As far as I remember, there's an issue floating around, which discusses this topic. Anyways, as you said, this is out of scope for this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's cargo-culting from other files that I copied from when I started out working on this code base. I think it and the test wording show that there might need to be some more definitions in the contribution guide about style and things to add or avoid.