diff --git a/idea/kotlin-android-plugin/src/org/jetbrains/kotlin/android/androidUtil.kt b/idea/kotlin-android-plugin/src/org/jetbrains/kotlin/android/androidUtil.kt new file mode 100644 index 00000000000..138fb7d9b91 --- /dev/null +++ b/idea/kotlin-android-plugin/src/org/jetbrains/kotlin/android/androidUtil.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2010-2016 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.android + +import com.intellij.psi.PsiElement +import org.jetbrains.android.facet.AndroidFacet + +fun PsiElement.getAndroidFacetForFile(): AndroidFacet? { + val file = containingFile ?: return null + return AndroidFacet.getInstance(file) +} \ No newline at end of file diff --git a/idea/kotlin-android-plugin/src/org/jetbrains/kotlin/android/inspection/IllegalIdentifierInspection.kt b/idea/kotlin-android-plugin/src/org/jetbrains/kotlin/android/inspection/IllegalIdentifierInspection.kt new file mode 100644 index 00000000000..f849a8e6aa5 --- /dev/null +++ b/idea/kotlin-android-plugin/src/org/jetbrains/kotlin/android/inspection/IllegalIdentifierInspection.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2010-2016 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.android.inspection + +import com.intellij.codeInsight.FileModificationService +import com.intellij.codeInspection.* +import com.intellij.ide.DataManager +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.refactoring.rename.RenameHandlerRegistry +import org.jetbrains.kotlin.android.getAndroidFacetForFile +import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* + +class IllegalIdentifierInspection : AbstractKotlinInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor { + return object : KtVisitorVoid() { + override fun visitElement(element: PsiElement) { + if (element.node.elementType != KtTokens.IDENTIFIER) return + + val text = element.text + // '`' can't be escaped now + if (!text.startsWith('`') || !text.endsWith('`')) return + + val unquotedName = KtPsiUtil.unquoteIdentifier(text) + // This is already an error + if (unquotedName.isEmpty()) return + + if (!unquotedName.all { isValidDalvikCharacter(it) } && checkAndroidFacet(element)) { + holder.registerProblem(element, + "Identifier not allowed in Android projects", + ProblemHighlightType.GENERIC_ERROR, + RenameIdentifierFix()) + } + } + + fun checkAndroidFacet(element: PsiElement): Boolean { + return element.getAndroidFacetForFile() != null || ApplicationManager.getApplication().isUnitTestMode + } + + // https://source.android.com/devices/tech/dalvik/dex-format.html#string-syntax + fun isValidDalvikCharacter(c: Char) = when (c) { + in 'A'..'Z' -> true + in 'a'..'z' -> true + in '0'..'9' -> true + '$', '-', '_' -> true + in '\u00a1' .. '\u1fff' -> true + in '\u2010' .. '\u2027' -> true + in '\u2030' .. '\ud7ff' -> true + in '\ue000' .. '\uffef' -> true + else -> false + } + } + } + + class RenameIdentifierFix : LocalQuickFix { + override fun getName() = "Rename" + override fun getFamilyName() = name + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val element = descriptor.psiElement ?: return + val file = element.containingFile ?: return + if (!FileModificationService.getInstance().prepareFileForWrite(file)) return + val editorManager = FileEditorManager.getInstance(project) + val editor = editorManager.getSelectedEditor(file.virtualFile) ?: return + val dataContext = DataManager.getInstance().getDataContext(editor.component) + val renameHandler = RenameHandlerRegistry.getInstance().getRenameHandler(dataContext) + renameHandler?.invoke(project, arrayOf(element), dataContext); + } + } +} \ No newline at end of file diff --git a/idea/kotlin-android-plugin/src/org/jetbrains/kotlin/android/navigation/KotlinAndroidGotoDeclarationHandler.java b/idea/kotlin-android-plugin/src/org/jetbrains/kotlin/android/navigation/KotlinAndroidGotoDeclarationHandler.java index d29d11c8a1d..0410dfb1624 100644 --- a/idea/kotlin-android-plugin/src/org/jetbrains/kotlin/android/navigation/KotlinAndroidGotoDeclarationHandler.java +++ b/idea/kotlin-android-plugin/src/org/jetbrains/kotlin/android/navigation/KotlinAndroidGotoDeclarationHandler.java @@ -21,7 +21,6 @@ import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.editor.Editor; import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; import com.intellij.psi.xml.XmlElement; import org.jetbrains.android.dom.AndroidAttributeValue; import org.jetbrains.android.dom.manifest.Manifest; @@ -35,6 +34,7 @@ import org.jetbrains.android.util.AndroidResourceUtil; import org.jetbrains.android.util.AndroidUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.kotlin.android.AndroidUtilKt; import org.jetbrains.kotlin.psi.KtSimpleNameExpression; import java.util.ArrayList; @@ -51,12 +51,7 @@ public class KotlinAndroidGotoDeclarationHandler implements GotoDeclarationHandl return null; } - PsiFile file = referenceExpression.getContainingFile(); - if (file == null) { - return null; - } - - AndroidFacet facet = AndroidFacet.getInstance(file); + AndroidFacet facet = AndroidUtilKt.getAndroidFacetForFile(referenceExpression); if (facet == null) { return null; } diff --git a/idea/src/META-INF/android.xml b/idea/src/META-INF/android.xml index ee848ec70b5..98485f2cf97 100644 --- a/idea/src/META-INF/android.xml +++ b/idea/src/META-INF/android.xml @@ -7,6 +7,12 @@ + + diff --git a/idea/testData/inspections/androidIllegalIdentifiers/inspectionData/expected.xml b/idea/testData/inspections/androidIllegalIdentifiers/inspectionData/expected.xml new file mode 100644 index 00000000000..937410f6457 --- /dev/null +++ b/idea/testData/inspections/androidIllegalIdentifiers/inspectionData/expected.xml @@ -0,0 +1,46 @@ + + + temp:///src/test.kt + 7 + light_idea_test_case + + Illegal Android Identifier + Illegal Android Identifier + + + + temp:///src/test.kt + 8 + light_idea_test_case + + Illegal Android Identifier + Illegal Android Identifier + + + + temp:///src/test.kt + 9 + light_idea_test_case + + Illegal Android Identifier + Illegal Android Identifier + + + + temp:///src/test.kt + 10 + light_idea_test_case + + Illegal Android Identifier + Illegal Android Identifier + + + + temp:///src/test.kt + 11 + light_idea_test_case + + Illegal Android Identifier + Illegal Android Identifier + + diff --git a/idea/testData/inspections/androidIllegalIdentifiers/inspectionData/inspections.test b/idea/testData/inspections/androidIllegalIdentifiers/inspectionData/inspections.test new file mode 100644 index 00000000000..ebd8e2d72f6 --- /dev/null +++ b/idea/testData/inspections/androidIllegalIdentifiers/inspectionData/inspections.test @@ -0,0 +1 @@ +// INSPECTION_CLASS: org.jetbrains.kotlin.android.inspection.IllegalIdentifierInspection diff --git a/idea/testData/inspections/androidIllegalIdentifiers/test.kt b/idea/testData/inspections/androidIllegalIdentifiers/test.kt new file mode 100644 index 00000000000..8e59b93137b --- /dev/null +++ b/idea/testData/inspections/androidIllegalIdentifiers/test.kt @@ -0,0 +1,11 @@ +val `Kotlin` = 0 +val `Котлин` = 0 +val `ことりん` = 0 +val `§*!`` = 0 +val `0a0b0c` = 0 +val `$a-b_c` = 0 +val `with space` = 0 //illegal +val `a+b` = 0 //illegal +val `#` = 0 //illegal +val `!` = 0 //illegal +val `()` = 0 //illegal \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java index 7e2585e5cda..3f5d4341517 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java @@ -106,6 +106,12 @@ public class InspectionTestGenerated extends AbstractInspectionTest { KotlinTestUtils.assertAllTestsPresentInSingleGeneratedClass(this.getClass(), new File("idea/testData/inspections"), Pattern.compile("^(inspections\\.test)$")); } + @TestMetadata("androidIllegalIdentifiers/inspectionData/inspections.test") + public void testAndroidIllegalIdentifiers_inspectionData_Inspections_test() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/androidIllegalIdentifiers/inspectionData/inspections.test"); + doTest(fileName); + } + @TestMetadata("conflictingExtensionProperty/inspectionData/inspections.test") public void testConflictingExtensionProperty_inspectionData_Inspections_test() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/conflictingExtensionProperty/inspectionData/inspections.test");