Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
INSP: add inspection to check associated type bindings
- Loading branch information
Showing
9 changed files
with
554 additions
and
7 deletions.
There are no files selected for viewing
117 changes: 117 additions & 0 deletions
117
src/main/kotlin/org/rust/ide/inspections/RsWrongAssocTypeArgumentsInspection.kt
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 |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/* | ||
* Use of this source code is governed by the MIT license that can be | ||
* found in the LICENSE file. | ||
*/ | ||
|
||
package org.rust.ide.inspections | ||
|
||
import com.intellij.psi.PsiElement | ||
import com.intellij.psi.util.parentOfType | ||
import org.rust.lang.core.completion.isFnLikeTrait | ||
import org.rust.lang.core.psi.* | ||
import org.rust.lang.core.psi.ext.RsAbstractableOwner | ||
import org.rust.lang.core.psi.ext.RsElement | ||
import org.rust.lang.core.psi.ext.owner | ||
import org.rust.lang.utils.RsDiagnostic | ||
import org.rust.lang.utils.addToHolder | ||
|
||
/** | ||
* Inspection that detects the E0191 and E0220 errors. | ||
*/ | ||
class RsWrongAssocTypeArgumentsInspection : RsLocalInspectionTool() { | ||
override fun buildVisitor(holder: RsProblemsHolder, isOnTheFly: Boolean) = | ||
object : RsVisitor() { | ||
override fun visitTraitRef(trait: RsTraitRef) { | ||
checkAssocTypes(holder, trait, trait.path) | ||
} | ||
|
||
override fun visitBaseType(type: RsBaseType) { | ||
val path = type.path ?: return | ||
if (path.referenceName == "Self") return | ||
|
||
checkAssocTypes(holder, type, path) | ||
} | ||
} | ||
|
||
private fun checkAssocTypes(holder: RsProblemsHolder, element: RsElement, path: RsPath) { | ||
val trait = path.reference?.resolve() as? RsTraitItem ?: return | ||
val arguments = path.typeArgumentList | ||
val assocArguments = arguments?.assocTypeBindingList | ||
|
||
val assocTypes = trait.associatedTypesTransitively.associateBy { it.identifier.text } | ||
val requiredAssocTypes = assocTypes.filter { it.value.typeReference == null } | ||
|
||
if (assocArguments != null) { | ||
checkUnknownAssocTypes(holder, assocArguments, assocTypes, trait) | ||
} | ||
|
||
val parent = element.parent | ||
// Do not check missing associated types in: | ||
// super trait and generic bounds | ||
// impl Trait for ... | ||
// Fn traits | ||
// impl Trait | ||
// type qual | ||
if (element.parentOfType<RsTypeParamBounds>() != null || | ||
(parent is RsImplItem && parent.traitRef == element) || | ||
trait.isFnLikeTrait || | ||
element.isImplTrait || | ||
element.parent is RsTypeQual) { | ||
return | ||
} | ||
|
||
checkMissingAssocTypes(holder, element, assocArguments, requiredAssocTypes) | ||
} | ||
|
||
private fun checkUnknownAssocTypes( | ||
holder: RsProblemsHolder, | ||
assocArguments: List<RsAssocTypeBinding>, | ||
assocTypes: Map<String, RsTypeAlias>, | ||
trait: RsTraitItem | ||
) { | ||
for (argument in assocArguments) { | ||
val name = argument.path.text | ||
if (name !in assocTypes) { | ||
val traitName = trait.name ?: continue | ||
RsDiagnostic.UnknownAssocTypeBinding(argument, name, traitName).addToHolder(holder) | ||
} | ||
} | ||
} | ||
|
||
private fun checkMissingAssocTypes( | ||
holder: RsProblemsHolder, | ||
element: RsElement, | ||
assocArguments: List<RsAssocTypeBinding>?, | ||
requiredAssocTypes: Map<String, RsTypeAlias> | ||
) { | ||
val assocArgumentMap = assocArguments?.associateBy { it.path.text }.orEmpty() | ||
val missingTypes = mutableListOf<MissingAssocTypeBinding>() | ||
for (type in requiredAssocTypes) { | ||
if (type.key !in assocArgumentMap) { | ||
val trait = (type.value.owner as? RsAbstractableOwner.Trait)?.trait ?: continue | ||
val traitName = trait.name ?: continue | ||
missingTypes.add(MissingAssocTypeBinding(type.key, traitName)) | ||
} | ||
} | ||
if (missingTypes.isNotEmpty()) { | ||
missingTypes.sortBy { it.name } | ||
RsDiagnostic.MissingAssocTypeBindings(element, missingTypes).addToHolder(holder) | ||
} | ||
} | ||
|
||
data class MissingAssocTypeBinding(val name: String, val trait: String) | ||
} | ||
|
||
/** | ||
* Detects | ||
* fn foo(_: impl Trait) | ||
*/ | ||
private val PsiElement.isImplTrait: Boolean | ||
get() { | ||
if (this !is RsTraitRef) return false | ||
val grandparent = this.parent.parent | ||
if (grandparent !is RsPolybound) return false | ||
|
||
val traitType = grandparent.parent as? RsTraitType ?: return false | ||
return traitType.impl != null | ||
} |
82 changes: 82 additions & 0 deletions
82
src/main/kotlin/org/rust/ide/inspections/fixes/AddAssocTypeBindingsFix.kt
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 |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* | ||
* Use of this source code is governed by the MIT license that can be | ||
* found in the LICENSE file. | ||
*/ | ||
|
||
package org.rust.ide.inspections.fixes | ||
|
||
import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement | ||
import com.intellij.openapi.editor.Editor | ||
import com.intellij.openapi.project.Project | ||
import com.intellij.psi.PsiElement | ||
import com.intellij.psi.PsiFile | ||
import org.rust.ide.utils.template.buildAndRunTemplate | ||
import org.rust.lang.core.psi.* | ||
import org.rust.lang.core.psi.ext.elementType | ||
import org.rust.lang.core.psi.ext.getNextNonCommentSibling | ||
import org.rust.openapiext.createSmartPointer | ||
|
||
class AddAssocTypeBindingsFix(element: PsiElement) : LocalQuickFixAndIntentionActionOnPsiElement(element) { | ||
override fun getText(): String = "Add missing associated types" | ||
override fun getFamilyName() = text | ||
|
||
override fun invoke( | ||
project: Project, | ||
file: PsiFile, | ||
editor: Editor?, | ||
startElement: PsiElement, | ||
endElement: PsiElement | ||
) { | ||
val element = startElement as? RsTraitRef ?: return | ||
val arguments = element.path.typeArgumentList | ||
val trait = element.path.reference?.resolve() as? RsTraitItem ?: return | ||
|
||
val requiredAssocTypes = trait.associatedTypesTransitively | ||
.filter { it.typeReference == null } | ||
.associateBy { it.identifier.text } | ||
val existingAssocTypes = arguments?.assocTypeBindingList | ||
.orEmpty() | ||
.associateBy { it.path.text } | ||
val missingAssocTypes = requiredAssocTypes.filter { it.key !in existingAssocTypes } | ||
if (missingAssocTypes.isEmpty()) return | ||
|
||
val factory = RsPsiFactory(project) | ||
val newAssocTypes = missingAssocTypes.toList().sortedBy { it.first } | ||
val defaultType = "()" | ||
|
||
val inserted = if (arguments != null) { | ||
var anchor = with(arguments) { | ||
assocTypeBindingList.lastOrNull() ?: typeReferenceList.lastOrNull() ?: lifetimeList.lastOrNull() ?: lt | ||
} | ||
val nextSibling = anchor.getNextNonCommentSibling() | ||
val addCommaAfter = nextSibling?.isComma == true | ||
if (addCommaAfter && nextSibling != null) { | ||
anchor = nextSibling | ||
} | ||
|
||
for (type in newAssocTypes) { | ||
if (anchor.elementType != RsElementTypes.LT && !anchor.isComma) { | ||
anchor = arguments.addAfter(factory.createComma(), anchor) | ||
} | ||
anchor = arguments.addAfter(factory.createAssocTypeBinding(type.first, defaultType), anchor) | ||
} | ||
|
||
if (addCommaAfter) { | ||
arguments.addAfter(factory.createComma(), anchor) | ||
} | ||
|
||
arguments | ||
} else { | ||
val newArgumentList = factory.createTypeArgumentList(newAssocTypes.map { "${it.first}=$defaultType" }) | ||
element.path.addAfter(newArgumentList, element.path.identifier) as RsTypeArgumentList | ||
} | ||
|
||
editor?.buildAndRunTemplate(element, inserted.assocTypeBindingList | ||
.takeLast(missingAssocTypes.size) | ||
.mapNotNull { it.typeReference?.createSmartPointer() } | ||
) | ||
} | ||
} | ||
|
||
private val PsiElement.isComma: Boolean | ||
get() = elementType == RsElementTypes.COMMA |
40 changes: 40 additions & 0 deletions
40
src/main/kotlin/org/rust/ide/inspections/fixes/RemoveAssocTypeBindingFix.kt
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 |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/* | ||
* Use of this source code is governed by the MIT license that can be | ||
* found in the LICENSE file. | ||
*/ | ||
|
||
package org.rust.ide.inspections.fixes | ||
|
||
import com.intellij.codeInspection.LocalQuickFixOnPsiElement | ||
import com.intellij.openapi.project.Project | ||
import com.intellij.psi.PsiElement | ||
import com.intellij.psi.PsiFile | ||
import com.intellij.psi.util.elementType | ||
import org.rust.lang.core.psi.RsAssocTypeBinding | ||
import org.rust.lang.core.psi.RsElementTypes.COMMA | ||
import org.rust.lang.core.psi.RsTypeArgumentList | ||
import org.rust.lang.core.psi.ext.getNextNonCommentSibling | ||
|
||
class RemoveAssocTypeBindingFix(binding: PsiElement) : LocalQuickFixOnPsiElement(binding) { | ||
override fun getFamilyName(): String = text | ||
override fun getText(): String = "Remove redundant associated type" | ||
|
||
override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { | ||
val binding = startElement as? RsAssocTypeBinding ?: return | ||
val parent = binding.parent as? RsTypeArgumentList | ||
|
||
val next = binding.getNextNonCommentSibling() | ||
binding.delete() | ||
|
||
if (next?.elementType == COMMA) { | ||
next?.delete() | ||
} | ||
|
||
if (parent != null && | ||
parent.typeReferenceList.isEmpty() && | ||
parent.assocTypeBindingList.isEmpty() && | ||
parent.lifetimeList.isEmpty()) { | ||
parent.delete() | ||
} | ||
} | ||
} |
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
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
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
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
8 changes: 8 additions & 0 deletions
8
src/main/resources/inspectionDescriptions/RsWrongAssocTypeArguments.html
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 |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<html> | ||
<body> | ||
Checks if correct associated type arguments were used for a type or a trait reference. | ||
<br/> | ||
Corresponds to the <a href="https://doc.rust-lang.org/error-index.html#E0191">E0191</a> and | ||
<a href="https://doc.rust-lang.org/error-index.html#E0220">E0220</a> Rust errors. | ||
</body> | ||
</html> |
Oops, something went wrong.