IDEA-218377 Support required named params

GitOrigin-RevId: 518cca90081b6567fabed432658c3f66c8b5a3d3
This commit is contained in:
alexey.afanasiev
2019-07-16 12:20:00 +03:00
committed by intellij-monorepo-bot
parent 38b39b952f
commit db3160961d
11 changed files with 125 additions and 25 deletions
@@ -74,6 +74,7 @@ cannot.apply.constructor=Constructor ''{0}'' in ''{1}'' cannot be applied to ''{
cannot.apply.default.constructor=Cannot apply default constructor for class ''{0}''
cannot.instantiate.abstract.class=Cannot instantiate abstract class ''{0}''
cannot.instantiate.interface=Cannot instantiate interface ''{0}''
missing.required.named.parameter=Missing required ''{0}'' named parameter
loss.of.precision=Possible loss of precision from ''{0}'' to ''{1}''
cannot.assign=Cannot assign ''{0}'' to ''{1}''
cannot.cast=Cannot cast ''{0}'' to ''{1}''
@@ -1,15 +1,19 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.groovy.annotator
import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiModifier
import org.jetbrains.plugins.groovy.GroovyBundle
import org.jetbrains.plugins.groovy.lang.psi.GroovyElementVisitor
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrCallExpression
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil
import org.jetbrains.plugins.groovy.transformations.immutable.hasImmutableAnnotation
import org.jetbrains.plugins.groovy.transformations.immutable.isImmutable
import org.jetbrains.plugins.groovy.transformations.impl.namedVariant.collectAllParamsFromNamedVariantMethod
import org.jetbrains.plugins.groovy.transformations.impl.namedVariant.collectNamedParams
/**
* Check features introduced in groovy 2.5
@@ -20,7 +24,10 @@ class GroovyAnnotator25(private val holder: AnnotationHolder) : GroovyElementVis
collectAllParamsFromNamedVariantMethod(method).groupBy { it.first }.filter { it.value.size > 1 }.forEach { (name, parameters) ->
val parametersList = parameters.joinToString { "'${it.second.name}'" }
parameters.drop(1).map { (_, parameter) -> parameter }.forEach {
holder.createErrorAnnotation(it.nameIdentifierGroovy, GroovyBundle.message("duplicating.named.parameter", name, parametersList))
val nameIdentifier = it.nameIdentifier
if (nameIdentifier != null) {
holder.createErrorAnnotation(nameIdentifier, GroovyBundle.message("duplicating.named.parameter", name, parametersList))
}
}
}
super.visitMethod(method)
@@ -37,4 +44,30 @@ class GroovyAnnotator25(private val holder: AnnotationHolder) : GroovyElementVis
holder.createErrorAnnotation(field.nameIdentifierGroovy, GroovyBundle.message("field.should.be.immutable", field.name))
}
}
override fun visitCallExpression(callExpression: GrCallExpression) {
checkRequiredNamedArguments(callExpression)
super.visitCallExpression(callExpression)
}
private fun checkRequiredNamedArguments(callExpression: GrCallExpression) {
val namedArguments = callExpression.namedArguments.mapNotNull { it.labelName }.toSet()
if (namedArguments.isEmpty()) return
val resolveResult = callExpression.advancedResolve()
val method = resolveResult.element as? PsiMethod ?: return
val parameters = method.parameterList.parameters
val mapParameter = (if (parameters.isNotEmpty()) parameters[0] else null) ?: return
val requiredNamedParams = collectNamedParams(mapParameter).filter { it.required }
requiredNamedParams.forEach {
if (!namedArguments.contains(it.name) && PsiUtil.isCompileStatic(callExpression)) {
val message = GroovyBundle.message("missing.required.named.parameter", it.name)
holder.createErrorAnnotation(callExpression, message)
}
}
}
}
@@ -9,19 +9,16 @@ import com.intellij.psi.PsiType;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.MethodSignature;
import com.intellij.util.ArrayUtilRt;
import kotlin.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.groovy.lang.psi.GrReferenceElement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
import org.jetbrains.plugins.groovy.lang.psi.impl.PsiImplUtil;
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;
import org.jetbrains.plugins.groovy.transformations.impl.namedVariant.NamedParamData;
import org.jetbrains.plugins.groovy.transformations.impl.namedVariant.NamedParamsUtil;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
/**
* @author ven
@@ -105,8 +102,8 @@ public class GroovyPresentationUtil {
}
private static boolean presentNamedParameters(@NotNull StringBuilder buffer, @NotNull GrParameter parameter) {
List<Pair<String, PsiType>> pairs = NamedParamsUtil.collectNamedParams(parameter);
StringUtil.join(pairs, pair -> pair.getFirst() + ": " + pair.getSecond().getPresentableText(), ", ", buffer);
List<NamedParamData> pairs = NamedParamsUtil.collectNamedParams(parameter);
StringUtil.join(pairs, namedParam -> namedParam.getName() + ": " + namedParam.getType().getPresentableText(), ", ", buffer);
return !pairs.isEmpty();
}
@@ -1,9 +1,10 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.groovy.transformations.impl.namedVariant
import com.intellij.psi.PsiMethod
import org.jetbrains.plugins.groovy.extensions.GroovyNamedArgumentProvider
import org.jetbrains.plugins.groovy.extensions.NamedArgumentDescriptor
import org.jetbrains.plugins.groovy.extensions.impl.NamedArgumentDescriptorImpl
import org.jetbrains.plugins.groovy.extensions.impl.TypeCondition
import org.jetbrains.plugins.groovy.lang.psi.api.GroovyResolveResult
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrCall
@@ -19,6 +20,9 @@ class GroovyNamedVariantArgumentProvider : GroovyNamedArgumentProvider() {
val parameters = method.parameterList.parameters
val mapParameter = (if (parameters.isNotEmpty()) parameters[0] else null) ?: return
collectNamedParams(mapParameter).forEach { result[it.first] = TypeCondition(it.second) }
collectNamedParams(mapParameter).forEach {
val type = it.type
result[it.name] = if (type != null) TypeCondition(type, it.navigation) else NamedArgumentDescriptorImpl(it.navigation)
}
}
}
@@ -1,7 +1,14 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.groovy.transformations.impl.namedVariant
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiParameter
import com.intellij.psi.PsiType
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter
internal data class NamedParamData(val name: String, val type: PsiType?, val origin: GrParameter)
data class NamedParamData(
val name: String,
val type: PsiType?,
val origin: PsiParameter,
val navigation: PsiElement,
val required: Boolean = false
)
@@ -1,4 +1,4 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.groovy.transformations.impl.namedVariant
import com.intellij.psi.CommonClassNames.JAVA_LANG_OBJECT
@@ -34,6 +34,7 @@ class NamedVariantTransformationSupport : AstTransformationSupport {
modifierList.addAnnotation(GROOVY_TRANSFORM_NAMED_PARAM).let {
it.addAttribute("type", namedParam.type?.presentableText ?: JAVA_LANG_OBJECT)
it.addAttribute("value", "\"${namedParam.name}\"")
it.addAttribute("required", "${namedParam.required}")
}
}
@@ -9,9 +9,9 @@ import com.intellij.psi.util.PropertyUtilBase
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotation
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod
import org.jetbrains.plugins.groovy.lang.psi.impl.GrAnnotationUtil
import org.jetbrains.plugins.groovy.lang.psi.impl.PsiImplUtil
import org.jetbrains.plugins.groovy.lang.psi.impl.getArrayValue
import org.jetbrains.plugins.groovy.lang.psi.util.GroovyPropertyUtils
@@ -25,7 +25,7 @@ const val GROOVY_TRANSFORM_NAMED_PARAMS = "groovy.transform.NamedParams"
const val GROOVY_TRANSFORM_NAMED_DELEGATE = "groovy.transform.NamedDelegate"
fun collectNamedParams(mapParameter: PsiParameter): List<Pair<String, PsiType>> {
fun collectNamedParams(mapParameter: PsiParameter): List<NamedParamData> {
if (!mapParameter.type.equalsToText(CommonClassNames.JAVA_UTIL_MAP)) return emptyList()
val annotations = mapParameter
@@ -34,25 +34,26 @@ fun collectNamedParams(mapParameter: PsiParameter): List<Pair<String, PsiType>>
?.getArrayValue { it as? GrAnnotation }
if (annotations != null) {
return annotations.mapNotNull(::constructNamedParameter)
return annotations.mapNotNull { constructNamedParameter(it, mapParameter) }
}
return mapParameter.annotations.mapNotNull(::constructNamedParameter)
return mapParameter.annotations.mapNotNull{ constructNamedParameter(it, mapParameter) }
}
private fun constructNamedParameter(annotation: PsiAnnotation): Pair<String, PsiType>? {
private fun constructNamedParameter(annotation: PsiAnnotation, owner: PsiParameter): NamedParamData? {
if(annotation.qualifiedName != GROOVY_TRANSFORM_NAMED_PARAM) return null
val attributeLiteral = annotation.findAttributeValue("value")
val name = (attributeLiteral as? GrLiteral)?.value as? String ?: return null
val classValue = annotation.findAttributeValue("type") as? GrExpression ?: return null
val type = ResolveUtil.getClassReferenceFromExpression(classValue) ?: return null
return name to type
val required = GrAnnotationUtil.inferBooleanAttribute(annotation, "required") ?: false
return NamedParamData(name, type, owner, annotation, required)
}
/**
* The order of the parameters is preserved as it is in the code
*/
fun collectAllParamsFromNamedVariantMethod(method: GrMethod): List<Pair<String, GrParameter>> {
fun collectAllParamsFromNamedVariantMethod(method: GrMethod): List<Pair<String, PsiParameter>> {
val namedParams = collectNamedParamsFromNamedVariantMethod(method).groupBy { it.origin }
return method.parameterList.parameters.flatMap { parameter ->
namedParams[parameter]?.let {
@@ -72,14 +73,15 @@ internal fun collectNamedParamsFromNamedVariantMethod(method: GrMethod): List<Na
val namedParamsAnn = PsiImplUtil.getAnnotation(parameter, GROOVY_TRANSFORM_NAMED_PARAM)
if (namedParamsAnn != null) {
val name = AnnotationUtil.getDeclaredStringAttributeValue(namedParamsAnn, "value") ?: parameter.name
result.add(NamedParamData(name, type, parameter))
val required = GrAnnotationUtil.inferBooleanAttribute(namedParamsAnn, "required") ?: false
result.add(NamedParamData(name, type, parameter, namedParamsAnn, required))
continue
}
PsiImplUtil.getAnnotation(parameter, GROOVY_TRANSFORM_NAMED_DELEGATE) ?: continue
val psiAnnotation = PsiImplUtil.getAnnotation(parameter, GROOVY_TRANSFORM_NAMED_DELEGATE) ?: continue
val parameterClass = (type as? PsiClassType)?.resolve() ?: continue
getProperties(parameterClass).forEach { (propertyName, propertyType) ->
result.add(NamedParamData(propertyName, propertyType, parameter))
result.add(NamedParamData(propertyName, propertyType, parameter, psiAnnotation))
}
}
return result
@@ -29,6 +29,12 @@ class Groovy25HighlightingTest extends LightGroovyTestCase implements Highlighti
highlightingTest()
}
void 'test named params required'() { highlightingTest() }
void 'test several absent required named params'() { highlightingTest() }
void 'test required named param in named variant'() { highlightingTest() }
void 'test named delegate without properties'() { highlightingTest() }
void 'test immutable fields'() { highlightingTest() }
@@ -0,0 +1,17 @@
import groovy.transform.CompileStatic
import groovy.transform.NamedParam
import groovy.transform.NamedParams
@CompileStatic
void namedParams(@NamedParams([
@NamedParam(value = 'first', type = String, required = true),
@NamedParam(value = 'last', type = Integer)
]) Map args, int i) {
print args.last
}
@CompileStatic
def m() {
<error descr="Missing required 'first' named parameter">namedParams(1, last: 1)</error>
}
@@ -0,0 +1,15 @@
import groovy.transform.CompileStatic
import groovy.transform.NamedParam
import groovy.transform.NamedVariant
@CompileStatic
@NamedVariant
void namedParams(int i, @NamedParam(value = 'param', required = true) Integer n) {
print i
}
@CompileStatic
def m() {
<error descr="Missing required 'param' named parameter">namedParams(1, a :1)</error>
}
@@ -0,0 +1,17 @@
import groovy.transform.CompileStatic
import groovy.transform.NamedParam
import groovy.transform.NamedParams
@CompileStatic
void namedParams(@NamedParams([
@NamedParam(value = 'first', type = String, required = true),
@NamedParam(value = 'last', type = Integer, required = true)
]) Map args, int i) {
print args.last
}
@CompileStatic
def m() {
<error descr="Missing required 'first' named parameter"><error descr="Missing required 'last' named parameter">namedParams(1, param: 1)</error></error>
}