diff --git a/idea/resources/inspectionDescriptions/KotlinInvalidBundleOrPropertyInspection.html b/idea/resources/inspectionDescriptions/KotlinInvalidBundleOrPropertyInspection.html new file mode 100644 index 00000000000..30833822ac8 --- /dev/null +++ b/idea/resources/inspectionDescriptions/KotlinInvalidBundleOrPropertyInspection.html @@ -0,0 +1,5 @@ + + +This inspection verifies that arguments passed to functions with parameters annotated as @PropertyKey are valid property keys in the respective properties files. It also verifies that the resourceBundle argument of the @PropertyKey annotation is an existing resource bundle. + + diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 53a61b4ae30..73595400b8a 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -1191,6 +1191,13 @@ level="WARNING" /> + + diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/KotlinInvalidBundleOrPropertyInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/KotlinInvalidBundleOrPropertyInspection.kt new file mode 100644 index 00000000000..4bd2c898b30 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/KotlinInvalidBundleOrPropertyInspection.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.inspections + +import com.intellij.codeInsight.CodeInsightBundle +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.codeInspection.i18n.JavaI18nUtil +import com.intellij.lang.properties.ResourceBundleReference +import com.intellij.lang.properties.psi.Property +import com.intellij.lang.properties.references.PropertyReference +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElementVisitor +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType +import org.jetbrains.kotlin.psi.psiUtil.parents +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.calls.model.ExpressionValueArgument +import org.jetbrains.kotlin.resolve.calls.model.VarargValueArgument +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull + +class KotlinInvalidBundleOrPropertyInspection : AbstractKotlinInspection() { + override fun getDisplayName() = CodeInsightBundle.message("inspection.unresolved.property.key.reference.name") + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : JetVisitorVoid() { + private fun processResourceBundleReference(ref: ResourceBundleReference, template: JetStringTemplateExpression) { + if (ref.resolve() == null) { + holder.registerProblem( + template, + CodeInsightBundle.message("inspection.invalid.resource.bundle.reference", ref.canonicalText), + ProblemHighlightType.LIKE_UNKNOWN_SYMBOL, + TextRange(0, template.textLength) + ) + } + } + + private fun processPropertyReference(ref: PropertyReference, template: JetStringTemplateExpression) { + val property = ref.resolve() as? Property + if (property == null) { + holder.registerProblem( + template, + CodeInsightBundle.message("inspection.unresolved.property.key.reference.message", ref.canonicalText), + ProblemHighlightType.LIKE_UNKNOWN_SYMBOL, + TextRange(0, template.textLength), + *ref.quickFixes + ) + return + } + + val argument = template.parents.firstIsInstanceOrNull() ?: return + if (argument.getArgumentExpression() != JetPsiUtil.deparenthesize(template) ) return + + val callExpression = argument.getStrictParentOfType() ?: return + val resolvedCall = callExpression.getResolvedCall(callExpression.analyze(BodyResolveMode.PARTIAL)) ?: return + + val resolvedArguments = resolvedCall.valueArgumentsByIndex ?: return + val keyArgumentIndex = resolvedArguments.indexOfFirst { it is ExpressionValueArgument && it.valueArgument == argument } + if (keyArgumentIndex < 0) return + + val callable = resolvedCall.resultingDescriptor + if (callable.valueParameters.size() != keyArgumentIndex + 2) return + + val messageArgument = resolvedArguments[keyArgumentIndex + 1] as? VarargValueArgument ?: return + if (messageArgument.arguments.singleOrNull()?.getSpreadElement() != null) return + + val expectedArgumentCount = JavaI18nUtil.getPropertyValuePlaceholdersCount(property.value ?: "") + val actualArgumentCount = messageArgument.arguments.size() + if (actualArgumentCount < expectedArgumentCount) { + val description = CodeInsightBundle.message( + "property.has.more.parameters.than.passed", + ref.canonicalText, expectedArgumentCount, actualArgumentCount + ) + holder.registerProblem(template, description, ProblemHighlightType.GENERIC_ERROR) + } + } + + override fun visitStringTemplateExpression(expression: JetStringTemplateExpression) { + for (ref in expression.references) { + when (ref) { + is ResourceBundleReference -> processResourceBundleReference(ref, expression) + is PropertyReference -> processPropertyReference(ref, expression) + } + } + } + } + } +} \ No newline at end of file diff --git a/idea/testData/multiFileInspections/invalidBundleOrProperty/before/PropertyKey.kt b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/PropertyKey.kt new file mode 100644 index 00000000000..1e1932b2d0f --- /dev/null +++ b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/PropertyKey.kt @@ -0,0 +1,5 @@ +package org.jetbrains.annotations + +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.LOCAL_VARIABLE, AnnotationTarget.FIELD) +public annotation class PropertyKey(public val resourceBundle: String) \ No newline at end of file diff --git a/idea/testData/multiFileInspections/invalidBundleOrProperty/before/TestBundle.properties b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/TestBundle.properties new file mode 100644 index 00000000000..d64999474d5 --- /dev/null +++ b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/TestBundle.properties @@ -0,0 +1,2 @@ +foo.bar = test: {0} +foo.bar2 = test: {0}, {1} \ No newline at end of file diff --git a/idea/testData/multiFileInspections/invalidBundleOrProperty/before/jMessage.java b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/jMessage.java new file mode 100644 index 00000000000..b90940210fa --- /dev/null +++ b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/jMessage.java @@ -0,0 +1,7 @@ +import org.jetbrains.annotations.PropertyKey; + +class J { + static String message(@PropertyKey(resourceBundle = "TestBundle") String key, Object... args) { + return key; + } +} \ No newline at end of file diff --git a/idea/testData/multiFileInspections/invalidBundleOrProperty/before/kMessage.kt b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/kMessage.kt new file mode 100644 index 00000000000..6323c3dc9f5 --- /dev/null +++ b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/kMessage.kt @@ -0,0 +1,6 @@ +import org.jetbrains.annotations.PropertyKey + +object K { + fun message(@PropertyKey(resourceBundle = "TestBundle") key: String, vararg args: Any) = key + fun message2(@PropertyKey(resourceBundle = "TestBundle") key: String, n: Int, vararg args: Any) = key +} \ No newline at end of file diff --git a/idea/testData/multiFileInspections/invalidBundleOrProperty/before/tooFewArguments.kt b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/tooFewArguments.kt new file mode 100644 index 00000000000..71ce3f85fd0 --- /dev/null +++ b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/tooFewArguments.kt @@ -0,0 +1,7 @@ +fun test() { + K.message("foo.bar") + J.message("foo.bar") + K.message(key = "foo.bar", args = 1) + K.message("foo.bar2", *arrayOf(1, 2)) + K.message2("foo.bar", 1) +} \ No newline at end of file diff --git a/idea/testData/multiFileInspections/invalidBundleOrProperty/before/unresolvedBundleReference.kt b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/unresolvedBundleReference.kt new file mode 100644 index 00000000000..af9b9058fa6 --- /dev/null +++ b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/unresolvedBundleReference.kt @@ -0,0 +1,6 @@ +import org.jetbrains.annotations.PropertyKey + +private val TEST_BUNDLE2 = "TestBundle2" + +fun message(@PropertyKey(resourceBundle = "TestBundle2") key: String, vararg args: Any) = key +fun message2(@PropertyKey(resourceBundle = TEST_BUNDLE2) key: String, vararg args: Any) = key \ No newline at end of file diff --git a/idea/testData/multiFileInspections/invalidBundleOrProperty/before/unresolvedPropertyReference.kt b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/unresolvedPropertyReference.kt new file mode 100644 index 00000000000..cac6edbf9b7 --- /dev/null +++ b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/unresolvedPropertyReference.kt @@ -0,0 +1,6 @@ +fun test() { + K.message("foo.baz", "arg") + K.message("foo.baz") + J.message("foo.baz", "arg") + J.message("foo.baz") +} \ No newline at end of file diff --git a/idea/testData/multiFileInspections/invalidBundleOrProperty/before/validPropertyKeys.kt b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/validPropertyKeys.kt new file mode 100644 index 00000000000..b9c2e1523cc --- /dev/null +++ b/idea/testData/multiFileInspections/invalidBundleOrProperty/before/validPropertyKeys.kt @@ -0,0 +1,6 @@ +fun test() { + K.message("foo.bar", "arg") + K.message("foo.bar", "arg1", "arg2") + J.message("foo.bar", "arg") + J.message("foo.bar", "arg1", "arg2") +} \ No newline at end of file diff --git a/idea/testData/multiFileInspections/invalidBundleOrProperty/expected.xml b/idea/testData/multiFileInspections/invalidBundleOrProperty/expected.xml new file mode 100644 index 00000000000..aa160f3a396 --- /dev/null +++ b/idea/testData/multiFileInspections/invalidBundleOrProperty/expected.xml @@ -0,0 +1,73 @@ + + + unresolvedPropertyReference.kt + 2 + testInvalidBundleOrProperty_InvalidBundleOrProperty_0 + + Invalid property key + String literal 'foo.baz' doesn't appear to be valid property key + + + + unresolvedPropertyReference.kt + 3 + testInvalidBundleOrProperty_InvalidBundleOrProperty_0 + + Invalid property key + String literal 'foo.baz' doesn't appear to be valid property key + + + + unresolvedPropertyReference.kt + 4 + testInvalidBundleOrProperty_InvalidBundleOrProperty_0 + + Invalid property key + String literal 'foo.baz' doesn't appear to be valid property key + + + + unresolvedPropertyReference.kt + 5 + testInvalidBundleOrProperty_InvalidBundleOrProperty_0 + + Invalid property key + String literal 'foo.baz' doesn't appear to be valid property key + + + + tooFewArguments.kt + 2 + testInvalidBundleOrProperty_InvalidBundleOrProperty_0 + + Invalid property key + Property 'foo.bar' expected 1 parameter, passed 0 + + + + tooFewArguments.kt + 3 + testInvalidBundleOrProperty_InvalidBundleOrProperty_0 + + Invalid property key + Property 'foo.bar' expected 1 parameter, passed 0 + + + + unresolvedBundleReference.kt + 3 + testInvalidBundleOrProperty_InvalidBundleOrProperty_0 + + Invalid property key + Invalid resource bundle reference 'TestBundle2' + + + + unresolvedBundleReference.kt + 5 + testInvalidBundleOrProperty_InvalidBundleOrProperty_0 + + Invalid property key + Invalid resource bundle reference 'TestBundle2' + + diff --git a/idea/testData/multiFileInspections/invalidBundleOrProperty/invalidBundleOrProperty.test b/idea/testData/multiFileInspections/invalidBundleOrProperty/invalidBundleOrProperty.test new file mode 100644 index 00000000000..3bec6249eb9 --- /dev/null +++ b/idea/testData/multiFileInspections/invalidBundleOrProperty/invalidBundleOrProperty.test @@ -0,0 +1,4 @@ +{ + "inspectionClass": "org.jetbrains.kotlin.idea.inspections.KotlinInvalidBundleOrPropertyInspection", + "withRuntime": "true" +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/JetMultiFileInspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/JetMultiFileInspectionTestGenerated.java index 134f8f52141..2501653a267 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/JetMultiFileInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/JetMultiFileInspectionTestGenerated.java @@ -35,6 +35,12 @@ public class JetMultiFileInspectionTestGenerated extends AbstractJetMultiFileIns JetTestUtils.assertAllTestsPresentInSingleGeneratedClass(this.getClass(), new File("idea/testData/multiFileInspections"), Pattern.compile("^(.+)\\.test$")); } + @TestMetadata("invalidBundleOrProperty/invalidBundleOrProperty.test") + public void testInvalidBundleOrProperty_InvalidBundleOrProperty() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/multiFileInspections/invalidBundleOrProperty/invalidBundleOrProperty.test"); + doTest(fileName); + } + @TestMetadata("mismatchedProjectAndDirectory/mismatchedProjectAndDirectory.test") public void testMismatchedProjectAndDirectory_MismatchedProjectAndDirectory() throws Exception { String fileName = JetTestUtils.navigationMetadata("idea/testData/multiFileInspections/mismatchedProjectAndDirectory/mismatchedProjectAndDirectory.test");