From 736cf76d4638e009f684a930e319192391ce7962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Fri, 30 Oct 2020 23:16:33 +0100 Subject: [PATCH] INSP: add inspection to check associated type bindings --- .../RsWrongAssocTypeArgumentsInspection.kt | 117 ++++++++ .../fixes/AddAssocTypeBindingsFix.kt | 82 ++++++ .../fixes/RemoveAssocTypeBindingFix.kt | 40 +++ .../lang/core/completion/LookupElements.kt | 2 +- .../org/rust/lang/core/psi/RsPsiFactory.kt | 9 + .../org/rust/lang/utils/RsDiagnostic.kt | 40 ++- src/main/resources/META-INF/rust-core.xml | 5 + .../RsWrongAssocTypeArguments.html | 8 + ...RsWrongAssocTypeArgumentsInspectionTest.kt | 258 ++++++++++++++++++ 9 files changed, 554 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/org/rust/ide/inspections/RsWrongAssocTypeArgumentsInspection.kt create mode 100644 src/main/kotlin/org/rust/ide/inspections/fixes/AddAssocTypeBindingsFix.kt create mode 100644 src/main/kotlin/org/rust/ide/inspections/fixes/RemoveAssocTypeBindingFix.kt create mode 100644 src/main/resources/inspectionDescriptions/RsWrongAssocTypeArguments.html create mode 100644 src/test/kotlin/org/rust/ide/inspections/RsWrongAssocTypeArgumentsInspectionTest.kt diff --git a/src/main/kotlin/org/rust/ide/inspections/RsWrongAssocTypeArgumentsInspection.kt b/src/main/kotlin/org/rust/ide/inspections/RsWrongAssocTypeArgumentsInspection.kt new file mode 100644 index 00000000000..3c31e83e5c8 --- /dev/null +++ b/src/main/kotlin/org/rust/ide/inspections/RsWrongAssocTypeArgumentsInspection.kt @@ -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() != 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, + assocTypes: Map, + 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?, + requiredAssocTypes: Map + ) { + val assocArgumentMap = assocArguments?.associateBy { it.path.text }.orEmpty() + val missingTypes = mutableListOf() + 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 + } diff --git a/src/main/kotlin/org/rust/ide/inspections/fixes/AddAssocTypeBindingsFix.kt b/src/main/kotlin/org/rust/ide/inspections/fixes/AddAssocTypeBindingsFix.kt new file mode 100644 index 00000000000..1c6944a6c2b --- /dev/null +++ b/src/main/kotlin/org/rust/ide/inspections/fixes/AddAssocTypeBindingsFix.kt @@ -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 diff --git a/src/main/kotlin/org/rust/ide/inspections/fixes/RemoveAssocTypeBindingFix.kt b/src/main/kotlin/org/rust/ide/inspections/fixes/RemoveAssocTypeBindingFix.kt new file mode 100644 index 00000000000..77ffc033c66 --- /dev/null +++ b/src/main/kotlin/org/rust/ide/inspections/fixes/RemoveAssocTypeBindingFix.kt @@ -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() + } + } +} diff --git a/src/main/kotlin/org/rust/lang/core/completion/LookupElements.kt b/src/main/kotlin/org/rust/lang/core/completion/LookupElements.kt index f1ca16cd7e4..b58a9ec661a 100644 --- a/src/main/kotlin/org/rust/lang/core/completion/LookupElements.kt +++ b/src/main/kotlin/org/rust/lang/core/completion/LookupElements.kt @@ -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 || diff --git a/src/main/kotlin/org/rust/lang/core/psi/RsPsiFactory.kt b/src/main/kotlin/org/rust/lang/core/psi/RsPsiFactory.kt index add522e909b..74d289ec5d5 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/RsPsiFactory.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/RsPsiFactory.kt @@ -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 " " diff --git a/src/main/kotlin/org/rust/lang/utils/RsDiagnostic.kt b/src/main/kotlin/org/rust/lang/utils/RsDiagnostic.kt index ab74a040c5e..235883151b6 100644 --- a/src/main/kotlin/org/rust/lang/utils/RsDiagnostic.kt +++ b/src/main/kotlin/org/rust/lang/utils/RsDiagnostic.kt @@ -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 @@ -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 + ) : 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, diff --git a/src/main/resources/META-INF/rust-core.xml b/src/main/resources/META-INF/rust-core.xml index 0758192fe8f..48fa788e4ae 100644 --- a/src/main/resources/META-INF/rust-core.xml +++ b/src/main/resources/META-INF/rust-core.xml @@ -660,6 +660,11 @@ enabledByDefault="true" level="WARNING" implementationClass="org.rust.ide.inspections.lints.RsUnnecessaryQualificationsInspection"/> + + + +Checks if correct associated type arguments were used for a type or a trait reference. +
+Corresponds to the E0191 and +E0220 Rust errors. + + diff --git a/src/test/kotlin/org/rust/ide/inspections/RsWrongAssocTypeArgumentsInspectionTest.kt b/src/test/kotlin/org/rust/ide/inspections/RsWrongAssocTypeArgumentsInspectionTest.kt new file mode 100644 index 00000000000..e04a56a521a --- /dev/null +++ b/src/test/kotlin/org/rust/ide/inspections/RsWrongAssocTypeArgumentsInspectionTest.kt @@ -0,0 +1,258 @@ +/* + * 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 org.rust.ProjectDescriptor +import org.rust.WithStdlibRustProjectDescriptor + +class RsWrongAssocTypeArgumentsInspectionTest : RsInspectionsTestBase(RsWrongAssocTypeArgumentsInspection::class) { + fun `test valid associated type`() = checkByText(""" + trait Foo { + type Bar; + } + fn foo(_: &dyn Foo) {} + """) + + fun `test valid supertrait associated type`() = checkByText(""" + trait A { + type A; + } + trait B: A {} + fn foo(_: &dyn B) {} + """) + + fun `test do not check missing types in supertrait`() = checkByText(""" + trait A { + type A; + } + trait B: A {} + """) + + fun `test do not check missing types in generic bound`() = checkByText(""" + trait Foo { + type A; + } + fn foo(t: T) {} + """) + + fun `test E0191 missing associated type`() = checkByText(""" + trait Trait { + type A; + } + fn foo(_: &dyn Trait) {} + """) + + fun `test E0191 missing associated type from supertrait`() = checkByText(""" + trait A { + type A; + } + trait B: A {} + fn foo(_: &dyn B) {} + """) + + fun `test E0191 multiple missing types`() = checkByText(""" + trait A { + type A; + } + trait B: A { + type B; + } + fn foo(_: &dyn B) {} + """) + + fun `test fix E0191 type without generic arguments`() = checkFixByText("Add missing associated types", """ + trait Foo { + type A; + } + fn foo(_: &dyn Foo/*caret*/) {} + """, """ + trait Foo { + type A; + } + fn foo(_: &dyn Foo) {} + """) + + fun `test fix E0191 type with generic arguments`() = checkFixByText("Add missing associated types", """ + trait Foo<'a, T> { + type A; + fn test(&self) -> &'a T; + } + fn foo<'a>(_: &dyn Foo<'a, u32>/*caret*/) {} + """, """ + trait Foo<'a, T> { + type A; + fn test(&self) -> &'a T; + } + fn foo<'a>(_: &dyn Foo<'a, u32, A=()>) {} + """) + + fun `test fix E0191 type with associated type arguments`() = checkFixByText("Add missing associated types", """ + trait Foo<'a, T> { + type A; + type B; + fn test(&self) -> &'a T; + } + fn foo<'a>(_: &dyn Foo<'a, u32, A=u32>/*caret*/) {} + """, """ + trait Foo<'a, T> { + type A; + type B; + fn test(&self) -> &'a T; + } + fn foo<'a>(_: &dyn Foo<'a, u32, A=u32, B=()>) {} + """) + + fun `test fix E0191 add types from supertraits`() = checkFixByText("Add missing associated types", """ + trait A { + type A; + } + trait B: A { + type B; + } + fn foo(_: &dyn B<>/*caret*/) {} + """, """ + trait A { + type A; + } + trait B: A { + type B; + } + fn foo(_: &dyn B) {} + """) + + fun `test E0220 redundant type`() = checkByText(""" + trait Trait {} + fn foo(_: &dyn Trait<A=u32>) {} + """) + + fun `test E0220 redundant type in supertrait`() = checkByText(""" + trait Trait {} + trait Trait2: Trait<A=u32> {} + """) + + fun `test E0220 redundant type in generic bound`() = checkByText(""" + trait Trait {} + fn fooA=u32>>(t: T) {} + """) + + fun `test fix E0220 redundant type`() = checkFixByText("Remove redundant associated type", """ + trait Trait {} + fn foo(_: &dyn Trait<A=u32/*caret*/>) {} + """, """ + trait Trait {} + fn foo(_: &dyn Trait) {} + """) + + fun `test fix E0220 redundant type remove comma after`() = checkFixByText("Remove redundant associated type", """ + trait Trait { + type Foo; + } + fn foo(_: &dyn Trait<A=u32/*caret*/, Foo=u32>) {} + """, """ + trait Trait { + type Foo; + } + fn foo(_: &dyn Trait) {} + """) + + fun `test fix E0220 impl trait target trait`() = checkFixByText("Remove redundant associated type", """ + trait Trait {} + trait Trait2 {} + impl Trait2 for Trait<A=u32/*caret*/> {} + """, """ + trait Trait {} + trait Trait2 {} + impl Trait2 for Trait {} + """) + + fun `test fix E0220 redundant type base type`() = checkFixByText("Remove redundant associated type", """ + trait Trait {} + fn foo() { + let x: Trait<A=u32/*caret*/>; + } + """, """ + trait Trait {} + fn foo() { + let x: Trait; + } + """) + + fun `test impl trait implemented trait`() = checkByText(""" + trait Trait { + type Foo; + } + impl Trait for () { + type Foo = u32; + } + """) + + fun `test E0191 impl trait target trait`() = checkByText(""" + trait Trait { + type Foo; + } + trait Trait2 {} + impl Trait2 for Trait {} + """) + + fun `test E0191 trait type without dyn`() = checkByText(""" + trait Trait { + type Foo; + } + fn foo() { + let x: Trait; + } + """) + + @ProjectDescriptor(WithStdlibRustProjectDescriptor::class) + fun `test fn-like traits`() = checkByText(""" + fn foo1(_: &mut dyn FnOnce(&mut [u8]) -> usize) {} + fn foo2(_: &mut dyn FnMut(&mut [u8]) -> usize) {} + fn foo3(_: &mut dyn Fn(&mut [u8]) -> usize) {} + """) + + fun `test self`() = checkByText(""" + trait Trait { + type FOO; + + fn foo() -> &'static Self { + unreachable!() + } + } + + fn foo1(_: &mut dyn FnOnce(&mut [u8]) -> usize) {} + fn foo2(_: &mut dyn FnMut(&mut [u8]) -> usize) {} + fn foo3(_: &mut dyn Fn(&mut [u8]) -> usize) {} + """) + + fun `test impl argument`() = checkByText(""" + trait Trait { + type FOO; + } + + fn foo(_: impl Trait) {} + """) + + fun `test impl return type`() = checkByText(""" + trait Trait { + type FOO; + } + + impl Trait for () { + type FOO = (); + } + + fn foo() -> impl Trait {} + """) + + fun `test type qual`() = checkByText(""" + trait Trait { + type FOO; + } + trait Trait2 {} + + fn foo(t: T) where ::FOO: Trait2 {} + """) +}