Skip to content

Commit

Permalink
INSP: add inspection to check associated type bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
Kobzol authored and dima74 committed Sep 8, 2022
1 parent c87fcd7 commit 736cf76
Show file tree
Hide file tree
Showing 9 changed files with 554 additions and 7 deletions.
@@ -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
}
@@ -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
@@ -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()
}
}
}
Expand Up @@ -411,7 +411,7 @@ private val InsertionContext.alreadyHasAngleBrackets: Boolean
private val InsertionContext.alreadyHasStructBraces: Boolean
get() = nextCharIs('{')

private val RsElement.isFnLikeTrait: Boolean
val RsElement.isFnLikeTrait: Boolean
get() {
val knownItems = knownItems
return this == knownItems.Fn ||
Expand Down
9 changes: 9 additions & 0 deletions src/main/kotlin/org/rust/lang/core/psi/RsPsiFactory.kt
Expand Up @@ -605,6 +605,15 @@ class RsPsiFactory(
fun createPat(patText: String): RsPat = tryCreatePat(patText) ?: error("Failed to create pat element")

fun tryCreatePat(patText: String): RsPat? = (createStatement("let $patText;") as RsLetDecl).pat

fun createAssocTypeBinding(name: String, type: String): RsAssocTypeBinding =
createFromText("""
trait Trait {
type $name;
}
fn foo(t: &dyn Trait<$name=$type>)
""")
?: error("Failed to created assoc type binding")
}

private fun String.iff(cond: Boolean) = if (cond) "$this " else " "
Expand Down
40 changes: 34 additions & 6 deletions src/main/kotlin/org/rust/lang/utils/RsDiagnostic.kt
Expand Up @@ -28,10 +28,8 @@ import org.rust.ide.annotator.fixes.*
import org.rust.ide.inspections.RsExperimentalChecksInspection
import org.rust.ide.inspections.RsProblemsHolder
import org.rust.ide.inspections.RsTypeCheckInspection
import org.rust.ide.inspections.fixes.AddMainFnFix
import org.rust.ide.inspections.fixes.AddRemainingArmsFix
import org.rust.ide.inspections.fixes.AddWildcardArmFix
import org.rust.ide.inspections.fixes.ChangeRefToMutableFix
import org.rust.ide.inspections.RsWrongAssocTypeArgumentsInspection
import org.rust.ide.inspections.fixes.*
import org.rust.ide.presentation.render
import org.rust.ide.presentation.renderInsertionSafe
import org.rust.ide.presentation.shortPresentableText
Expand Down Expand Up @@ -1570,12 +1568,42 @@ sealed class RsDiagnostic(
fixes = listOfNotNull(fix)
)
}

class UnknownAssocTypeBinding(element: RsAssocTypeBinding,
private val name: String,
private val trait: String
) : RsDiagnostic(element) {
override fun prepare(): PreparedAnnotation = PreparedAnnotation(
ERROR,
E0220,
"Associated type `$name` not found for `$trait`",
fixes = listOf(RemoveAssocTypeBindingFix(element))
)
}

class MissingAssocTypeBindings(
element: PsiElement,
private val missingTypes: List<RsWrongAssocTypeArgumentsInspection.MissingAssocTypeBinding>
) : RsDiagnostic(element) {
private fun getText(): String {
val typeText = pluralize("type", missingTypes.size)
val missing = missingTypes.joinToString(", ") { "`${it.name}` (from trait `${it.trait}`)" }
return "The value of the associated $typeText $missing must be specified"
}

override fun prepare(): PreparedAnnotation = PreparedAnnotation(
ERROR,
E0191,
getText(),
fixes = listOf(AddAssocTypeBindingsFix(element))
)
}
}

enum class RsErrorCode {
E0004, E0013, E0015, E0023, E0025, E0026, E0027, E0040, E0046, E0049, E0050, E0054, E0057, E0060, E0061, E0069, E0081, E0084,
E0106, E0107, E0116, E0117, E0118, E0120, E0121, E0124, E0132, E0133, E0184, E0185, E0186, E0198, E0199,
E0200, E0201, E0252, E0254, E0255, E0259, E0260, E0261, E0262, E0263, E0267, E0268, E0277,
E0106, E0107, E0116, E0117, E0118, E0120, E0121, E0124, E0132, E0133, E0184, E0185, E0186, E0191, E0198, E0199,
E0200, E0201, E0220, E0252, E0254, E0255, E0259, E0260, E0261, E0262, E0263, E0267, E0268, E0277,
E0308, E0322, E0328, E0364, E0365, E0379, E0384,
E0403, E0404, E0407, E0415, E0416, E0424, E0426, E0428, E0429, E0430, E0431, E0433, E0434, E0435, E0449, E0451, E0463,
E0517, E0518, E0537, E0552, E0554, E0562, E0569, E0583, E0586, E0594,
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/META-INF/rust-core.xml
Expand Up @@ -660,6 +660,11 @@
enabledByDefault="true" level="WARNING"
implementationClass="org.rust.ide.inspections.lints.RsUnnecessaryQualificationsInspection"/>

<localInspection language="Rust" groupName="Rust"
displayName="Wrong associated type arguments"
enabledByDefault="true" level="ERROR"
implementationClass="org.rust.ide.inspections.RsWrongAssocTypeArgumentsInspection"/>

<!-- Surrounders -->

<lang.surroundDescriptor language="Rust"
Expand Down
@@ -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>

0 comments on commit 736cf76

Please sign in to comment.