From 3caa07b67a320f688a4cd14e4c4a28392157b299 Mon Sep 17 00:00:00 2001 From: Alexey Kudravtsev Date: Wed, 6 Apr 2022 14:04:13 +0200 Subject: [PATCH] Non-ASCII characters inspection: introduce new option "mixed languages in any word" to report this kind of problem in any language, fix incorrect mapping UI<-> CHECK field, to fix IDEA-291262 Report words with mixed charsets GitOrigin-RevId: e22ebd3b1d11683b38280d254da8e8083d0770f1 --- .../nonAsciiCharacters/Groovy.groovy | 12 +- .../MixedLanguagesJavaInAnyWord.java | 13 + .../MixedLanguagesJavaInVariousContexts.java | 13 + .../MixedLanguagesXMLInAnyWord.xml | 6 + ...MixedLanguagesXMLInAnyWordExceptString.xml | 6 + .../NotAsciiJavaInAnyWord.java | 12 + .../NotAsciiJavaInVariousContexts.java | 12 + .../nonAsciiCharacters/Simple.java | 12 - .../daemon/NonAsciiCharactersTest.java | 100 +++++- .../messages/CodeInsightBundle.properties | 38 ++- .../resources/messages/LangBundle.properties | 4 - .../NonAsciiCharactersInspection.java | 294 +++++++++++++----- .../NonAsciiCharactersInspectionFormUi.kt | 72 +++-- 13 files changed, 454 insertions(+), 140 deletions(-) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesJavaInAnyWord.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesJavaInVariousContexts.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesXMLInAnyWord.xml create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesXMLInAnyWordExceptString.xml create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/NotAsciiJavaInAnyWord.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/NotAsciiJavaInVariousContexts.java delete mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/Simple.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/Groovy.groovy b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/Groovy.groovy index 6b81f6869e01..a458a5124a24 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/Groovy.groovy +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/Groovy.groovy @@ -1,9 +1,9 @@ class X { - int Ж = 0; - class InnerП {} - // commentжп 234 - String s = "12л3орыва0"; - void жжж() { - жжж(); + int Ж = 0; + class InnerП {} + // commentжп 234 + String s = "12лTOорыва0"; + void жжж() { + жжж(); } } \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesJavaInAnyWord.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesJavaInAnyWord.java new file mode 100644 index 000000000000..2981bf1232c5 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesJavaInAnyWord.java @@ -0,0 +1,13 @@ +class X { + int Ж = 0; + class InnerП {} + // commentжп 234 + String sameLang = "12л3орыва0"; + String mixed = "12чеto3"; + void жжж() { + жжж(); + String core = "Сore"; + System.out.println("Сore"); + String u = "\5\0\51\2\3\0\136\2\21\0\33\2\65\0\20\2\u0200\0\u19b6\2"; // ignore slash u + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesJavaInVariousContexts.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesJavaInVariousContexts.java new file mode 100644 index 000000000000..2981bf1232c5 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesJavaInVariousContexts.java @@ -0,0 +1,13 @@ +class X { + int Ж = 0; + class InnerП {} + // commentжп 234 + String sameLang = "12л3орыва0"; + String mixed = "12чеto3"; + void жжж() { + жжж(); + String core = "Сore"; + System.out.println("Сore"); + String u = "\5\0\51\2\3\0\136\2\21\0\33\2\65\0\20\2\u0200\0\u19b6\2"; // ignore slash u + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesXMLInAnyWord.xml b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesXMLInAnyWord.xml new file mode 100644 index 000000000000..c7ea407844c7 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesXMLInAnyWord.xml @@ -0,0 +1,6 @@ +Ш> + блабла + Ы attr="ИИИИ23" attr2="ИИИfoo"> + блаfoo + Ы> +Ш> \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesXMLInAnyWordExceptString.xml b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesXMLInAnyWordExceptString.xml new file mode 100644 index 000000000000..307a7e14e242 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/MixedLanguagesXMLInAnyWordExceptString.xml @@ -0,0 +1,6 @@ +Ш> + блабла + Ы attr="ИИИИ23" attr2="ИИИfoo"> + блаfoo + Ы> +Ш> \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/NotAsciiJavaInAnyWord.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/NotAsciiJavaInAnyWord.java new file mode 100644 index 000000000000..e36f5781c74f --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/NotAsciiJavaInAnyWord.java @@ -0,0 +1,12 @@ +class X { + int Ж = 0; + class InnerП {} + // commentжп 234 + String s = "12л3орыва0"; + void жжж() { + жжж(); + String s = "Сore"; + System.out.println("Сore"); + String u = "\5\0\51\2\3\0\136\2\21\0\33\2\65\0\20\2\u0200\0\u19b6\2"; // ignore slash u + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/NotAsciiJavaInVariousContexts.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/NotAsciiJavaInVariousContexts.java new file mode 100644 index 000000000000..e36f5781c74f --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/NotAsciiJavaInVariousContexts.java @@ -0,0 +1,12 @@ +class X { + int Ж = 0; + class InnerП {} + // commentжп 234 + String s = "12л3орыва0"; + void жжж() { + жжж(); + String s = "Сore"; + System.out.println("Сore"); + String u = "\5\0\51\2\3\0\136\2\21\0\33\2\65\0\20\2\u0200\0\u19b6\2"; // ignore slash u + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/Simple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/Simple.java deleted file mode 100644 index 816a2eb2c236..000000000000 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/Simple.java +++ /dev/null @@ -1,12 +0,0 @@ -class X { - int Ж = 0; - class InnerП {} - // commentжп 234 - String s = "12л3орыва0"; - void жжж() { - жжж(); - String s = "Сore"; - System.out.println("Сore"); - String u = "\5\0\51\2\3\0\136\2\21\0\33\2\65\0\20\2\u0200\0\u19b6\2"; // ignore slash u - } -} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/NonAsciiCharactersTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/NonAsciiCharactersTest.java index 10d68e137b40..6ca6757626a7 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/NonAsciiCharactersTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/NonAsciiCharactersTest.java @@ -9,17 +9,17 @@ import org.jetbrains.annotations.NotNull; public class NonAsciiCharactersTest extends DaemonAnalyzerTestCase { private static final String BASE_PATH = "/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters"; + private NonAsciiCharactersInspection myInspection = new NonAsciiCharactersInspection(); @Override protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() { - NonAsciiCharactersInspection inspection = new NonAsciiCharactersInspection(); - inspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME = true; - inspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING = true; - inspection.CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME = true; - inspection.CHECK_FOR_NOT_ASCII_COMMENT = true; - inspection.CHECK_FOR_NOT_ASCII_STRING_LITERAL = true; - inspection.CHECK_FOR_FILES_CONTAINING_BOM = true; - return new LocalInspectionTool[]{inspection}; + return new LocalInspectionTool[]{myInspection}; + } + + @Override + protected void tearDown() throws Exception { + myInspection = null; + super.tearDown(); } private void doTest(String extension) throws Exception { @@ -27,10 +27,92 @@ public class NonAsciiCharactersTest extends DaemonAnalyzerTestCase { UIUtil.dispatchAllInvocationEvents(); } - public void testSimple() throws Exception { + public void testNotAsciiJavaInVariousContexts() throws Exception { + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_ANY_OTHER_WORD = false; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME = false; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_COMMENTS = false; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING = false; + myInspection.CHECK_FOR_NOT_ASCII_IN_ANY_OTHER_WORD = false; + myInspection.CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME = true; + myInspection.CHECK_FOR_NOT_ASCII_COMMENT = true; + myInspection.CHECK_FOR_NOT_ASCII_STRING_LITERAL = true; + myInspection.CHECK_FOR_FILES_CONTAINING_BOM = false; doTest(".java"); } + public void testNotAsciiJavaInAnyWord() throws Exception { + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_ANY_OTHER_WORD = false; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME = false; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_COMMENTS = false; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING = false; + myInspection.CHECK_FOR_NOT_ASCII_IN_ANY_OTHER_WORD = true; + myInspection.CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME = true; + myInspection.CHECK_FOR_NOT_ASCII_COMMENT = true; + myInspection.CHECK_FOR_NOT_ASCII_STRING_LITERAL = true; + myInspection.CHECK_FOR_FILES_CONTAINING_BOM = false; + doTest(".java"); + } + + public void testMixedLanguagesJavaInAnyWord() throws Exception { + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_ANY_OTHER_WORD = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_COMMENTS = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING = true; + myInspection.CHECK_FOR_NOT_ASCII_IN_ANY_OTHER_WORD = false; + myInspection.CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME = false; + myInspection.CHECK_FOR_NOT_ASCII_COMMENT = false; + myInspection.CHECK_FOR_NOT_ASCII_STRING_LITERAL = false; + myInspection.CHECK_FOR_FILES_CONTAINING_BOM = false; + doTest(".java"); + } + + public void testMixedLanguagesXMLInAnyWord() throws Exception { + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_ANY_OTHER_WORD = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_COMMENTS = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING = true; + myInspection.CHECK_FOR_NOT_ASCII_IN_ANY_OTHER_WORD = false; + myInspection.CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME = false; + myInspection.CHECK_FOR_NOT_ASCII_COMMENT = false; + myInspection.CHECK_FOR_NOT_ASCII_STRING_LITERAL = false; + myInspection.CHECK_FOR_FILES_CONTAINING_BOM = false; + doTest(".xml"); + } + public void testMixedLanguagesXMLInAnyWordExceptString() throws Exception { + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_ANY_OTHER_WORD = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_COMMENTS = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING = false; + myInspection.CHECK_FOR_NOT_ASCII_IN_ANY_OTHER_WORD = false; + myInspection.CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME = false; + myInspection.CHECK_FOR_NOT_ASCII_COMMENT = false; + myInspection.CHECK_FOR_NOT_ASCII_STRING_LITERAL = false; + myInspection.CHECK_FOR_FILES_CONTAINING_BOM = false; + doTest(".xml"); + } + + public void testMixedLanguagesJavaInVariousContexts() throws Exception { + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_ANY_OTHER_WORD = false; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_COMMENTS = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING = true; + myInspection.CHECK_FOR_NOT_ASCII_IN_ANY_OTHER_WORD = false; + myInspection.CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME = false; + myInspection.CHECK_FOR_NOT_ASCII_COMMENT = false; + myInspection.CHECK_FOR_NOT_ASCII_STRING_LITERAL = false; + myInspection.CHECK_FOR_FILES_CONTAINING_BOM = false; + doTest(".java"); + } + public void testGroovy() throws Exception { + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_ANY_OTHER_WORD = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_COMMENTS = true; + myInspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING = true; + myInspection.CHECK_FOR_NOT_ASCII_IN_ANY_OTHER_WORD = true; + myInspection.CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME = true; + myInspection.CHECK_FOR_NOT_ASCII_COMMENT = true; + myInspection.CHECK_FOR_NOT_ASCII_STRING_LITERAL = true; + myInspection.CHECK_FOR_FILES_CONTAINING_BOM = false; doTest(".groovy"); } } diff --git a/platform/lang-api/resources/messages/CodeInsightBundle.properties b/platform/lang-api/resources/messages/CodeInsightBundle.properties index 2367c5a0cb7e..a5e8bab79091 100644 --- a/platform/lang-api/resources/messages/CodeInsightBundle.properties +++ b/platform/lang-api/resources/messages/CodeInsightBundle.properties @@ -512,19 +512,31 @@ actions.on.save.page.popup.title.arrangement.settings=Open Arrangement Settings actions.on.save.page.checkbox.run.code.cleanup=Run code cleanup actions.on.save.page.code.cleanup.comment=Applies fixes from the code cleanup inspections actions.on.save.page.link.configure.inspections=Configure inspections... -non.ascii.warn.of=Warn of: -label.non.ascii.chars.example.files.containing.bom=E.g. files starting with FEFF -label.non.ascii.chars.example.characters.in.comments=E.g.: // hello \u10D2\u10D4\u10DC\u10D0\u10EA\u10D5\u10D0\u10DA\u10D8 -label.non.ascii.chars.example.characters.in.identifiers=E.g.: int \u041F\u043E\u043B\u0435 = 0; -label.non.ascii.chars.example.characters.in.strings=E.g.: println("\u10D2\u10D4\u10DC\u10D0\u10EA\u10D5\u10D0\u10DA\u10D8"); -label.non.ascii.chars.example.different.languages.in.identifiers=E.g.: void print\u0415\u0440\u0443\u043D\u0434\u0430() -label.non.ascii.chars.example.different.languages.in.string=E.g.: foo("\u0421ore"); //cyrillic \u0421 -checkbox.non.ascii.option.different.languages.in.identifiers=Different languages in identifiers -checkbox.non.ascii.option.different.languages.in.string=Different languages in strings -checkbox.non.ascii.option.files.containing.bom=Files containing BOM -checkbox.non.ascii.option.characters.in.identifiers=Non-ASCII characters in identifiers -checkbox.non.ascii.option.characters.in.comments=Non-ASCII characters in comments -checkbox.non.ascii.option.characters.in.strings=Non-ASCII characters in strings + +non.ascii.chars.inspection.non.ascii.top.label=Warn of non-ASCII characters in: +non.ascii.chars.inspection.option.files.containing.bom.checkbox=Warn of files containing BOM (e.g., files starting with 'FEFF' bytes) +non.ascii.chars.inspection.option.characters.in.any.other.word.checkbox=any other word +non.ascii.chars.inspection.example.characters.in.any.other.word.label=(e.g.: \\<\u0422\u042D\u0413-\u0428\u041C\u042D\u0413\\>0\\</\u0422\u042D\u0413-\u0428\u041C\u042D\u0413\\>) +non.ascii.chars.inspection.option.characters.in.identifiers.checkbox=identifiers +non.ascii.chars.inspection.example.characters.in.identifiers.label=(e.g.: int \u041F\u043E\u043B\u0435 = 0;) +non.ascii.chars.inspection.option.characters.in.comments.checkbox=comments +non.ascii.chars.inspection.example.characters.in.comments.label=(e.g.: // \u10D2\u10D4\u10DC\u10D0\u10EA\u10D5\u10D0\u10DA\u10D8) +non.ascii.chars.inspection.option.characters.in.strings.checkbox=strings +non.ascii.chars.inspection.example.characters.in.strings.label=(e.g.: println("\u10D2\u10D4\u10DC\u10D0\u10EA\u10D5\u10D0\u10DA\u10D8")) +non.ascii.chars.inspection.mixed.chars.top.label=Warn of mixed language characters in: +non.ascii.chars.inspection.option.mixed.languages.in.any.other.word.checkbox=any other word +non.ascii.chars.inspection.example.mixed.languages.in.any.other.word.label=(e.g.: \\<t\u0430g\\>0\\</t\u0430g\\> (cyrillic '\u0430')) +non.ascii.chars.inspection.option.mixed.languages.in.identifiers.checkbox=identifiers +non.ascii.chars.inspection.example.mixed.languages.in.identifiers.label=(e.g.: void print\u0415\u0440\u0443\u043D\u0434\u0430()) +non.ascii.chars.inspection.option.mixed.languages.in.comments.checkbox=comments +non.ascii.chars.inspection.example.mixed.languages.in.comments.label=(e.g.: //c\u043Eunts bugs (cyrillic 'o')) +non.ascii.chars.inspection.option.mixed.languages.in.strings.checkbox=strings +non.ascii.chars.inspection.example.mixed.languages.in.string.label=(e.g.: foo("\u0421ore"); //cyrillic '\u0421' ) +non.ascii.chars.inspection.message.file.contains.bom=File contains BOM: ''{0}''{1} +non.ascii.chars.inspection.message.charset.signature=\ (charset ''{0}'' signature) +non.ascii.chars.inspection.message.non.ascii.characters=Non-ASCII characters +non.ascii.chars.inspection.message.symbols.from.different.languages.found=Mixed languages: {0} symbols found in {1} word + label.file.included.into=Included into: checkbox.reformat.project.file.masks=&File mask(s) checkbox.reformat.project.include.subdirectories=&Include subdirectories diff --git a/platform/lang-api/resources/messages/LangBundle.properties b/platform/lang-api/resources/messages/LangBundle.properties index 27079d4bf6ca..f139b7a90fd7 100644 --- a/platform/lang-api/resources/messages/LangBundle.properties +++ b/platform/lang-api/resources/messages/LangBundle.properties @@ -100,10 +100,6 @@ dialog.message.live.template.with.empty.abbreviation=A live template with an emp dialog.message.live.template.with.empty.text=A live template ''{0}'' with an empty text has been found in ''{1}'' group, such live templates make no sense dialog.message.duplicate.live.templates.in.group=Duplicate `{0}` live templates in {1} group popup.advertisement.press.or.to.replace=Press {0} or {1} to replace -inspection.message.file.contains.bom=File contains BOM: ''{0}''{1} -inspection.message.charset.signature=\ (charset ''{0}'' signature) -inspection.message.non.ascii.characters.in=Non-ASCII characters in {0, choice, 0#an identifier|1#a comment|2#a string literal} -inspection.message.symbols.from.different.languages.found=Symbols from different languages found: {0} hint.text.unfortunately.currently.available.for.batch.mode=Unfortunately ''{0}'' is currently not available for batch mode\n User interaction is required for each problem found progress.title.inspect.code=Running Code Cleanup with profile ''{0}''\u2026 hint.text.can.t.modify.decompiled.code=Can't modify decompiled code diff --git a/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspection.java b/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspection.java index 4ea0f78a784c..8790be06bf77 100644 --- a/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspection.java +++ b/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspection.java @@ -2,26 +2,31 @@ package com.intellij.codeInspection; -import com.intellij.lang.LangBundle; +import com.intellij.codeInsight.CodeInsightBundle; +import com.intellij.lang.Commenter; +import com.intellij.lang.Language; +import com.intellij.lang.LanguageCommenters; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.lang.properties.charset.Native2AsciiCharset; +import com.intellij.openapi.editor.colors.TextAttributesKey; import com.intellij.openapi.fileEditor.impl.LoadTextUtil; +import com.intellij.openapi.fileTypes.SyntaxHighlighter; +import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; -import com.intellij.util.io.IOUtil; +import com.intellij.psi.impl.source.tree.ForeignLeafPsiElement; +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.usages.ChunkExtractor; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -29,9 +34,12 @@ public class NonAsciiCharactersInspection extends LocalInspectionTool { public boolean CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME = true; public boolean CHECK_FOR_NOT_ASCII_STRING_LITERAL; public boolean CHECK_FOR_NOT_ASCII_COMMENT; - + public boolean CHECK_FOR_NOT_ASCII_IN_ANY_OTHER_WORD; + public boolean CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME = true; public boolean CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING; + public boolean CHECK_FOR_DIFFERENT_LANGUAGES_IN_COMMENTS; + public boolean CHECK_FOR_DIFFERENT_LANGUAGES_IN_ANY_OTHER_WORD = true; public boolean CHECK_FOR_FILES_CONTAINING_BOM; @Override @@ -51,40 +59,63 @@ public class NonAsciiCharactersInspection extends LocalInspectionTool { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly, @NotNull LocalInspectionToolSession session) { - if (!isFileWorthIt(session.getFile())) return PsiElementVisitor.EMPTY_VISITOR; + PsiFile file = session.getFile(); + if (!isFileWorthIt(file)) return PsiElementVisitor.EMPTY_VISITOR; + SyntaxHighlighter syntaxHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(file.getFileType(), file.getProject(), file.getVirtualFile()); return new PsiElementVisitor() { @Override public void visitElement(@NotNull PsiElement element) { - if (CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME || CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME) { - PsiElement parent = element.getParent(); - PsiElement identifier; - if (parent instanceof PsiNameIdentifierOwner && - (identifier = ((PsiNameIdentifierOwner)parent).getNameIdentifier()) != null) { - // Groovy has this twisted PSI where method.geNameIdentifier() is some random light element - String text = element.getText(); - if (identifier == element || text.equals(identifier.getText())) { - if (CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME) { - checkAscii(element, text, holder); + if (!(element instanceof LeafPsiElement) + // optimization: ignore very frequent white space element + || element instanceof PsiWhiteSpace) { + return; + } + + PsiElementKind kind = getKind(element, syntaxHighlighter); + TextRange valueRange = null; // the range inside element with the actual contents with quotes/comment prefixes stripped out + switch (kind) { + case STRING: + if (CHECK_FOR_NOT_ASCII_STRING_LITERAL || CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING) { + String text = element.getText(); + valueRange = StringUtil.isQuotedString(text) ? new TextRange(1, text.length() - 1) : null; + if (CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING) { + reportMixedLanguages(element, text, holder, valueRange); } - if (CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME) { - checkSameLanguage(element, text, holder); + if (CHECK_FOR_NOT_ASCII_STRING_LITERAL) { + reportNonAsciiRange(element, text, holder, valueRange); } } - } - } - if (CHECK_FOR_NOT_ASCII_COMMENT) { - if (element instanceof PsiComment) { - checkAsciiRange(element, element.getText(), holder, Kind.COMMENT); - } - } - if (element instanceof PsiLiteralValue) { - String text = element.getText(); - if (CHECK_FOR_NOT_ASCII_STRING_LITERAL) { - checkAsciiRange(element, text, holder, Kind.LITERAL); - } - if (CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING) { - checkSameLanguage(element, text, holder); - } + break; + case IDENTIFIER: + if (CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME) { + reportNonAsciiRange(element, element.getText(), holder, null); + } + if (CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME) { + reportMixedLanguages(element, element.getText(), holder, null); + } + break; + case COMMENT: + if (CHECK_FOR_NOT_ASCII_COMMENT || CHECK_FOR_DIFFERENT_LANGUAGES_IN_COMMENTS) { + String text = element.getText(); + valueRange = getCommentRange(element, text); + if (CHECK_FOR_NOT_ASCII_COMMENT) { + reportNonAsciiRange(element, text, holder, valueRange); + } + if (CHECK_FOR_DIFFERENT_LANGUAGES_IN_COMMENTS) { + reportMixedLanguages(element, text, holder, valueRange); + } + } + break; + case OTHER: + if (CHECK_FOR_NOT_ASCII_IN_ANY_OTHER_WORD) { + String text = element.getText(); + reportNonAsciiRange(element, text, holder, valueRange); + } + if (CHECK_FOR_DIFFERENT_LANGUAGES_IN_ANY_OTHER_WORD) { + String text = element.getText(); + reportMixedLanguages(element, text, holder, valueRange); + } + break; } } @@ -92,28 +123,72 @@ public class NonAsciiCharactersInspection extends LocalInspectionTool { public void visitFile(@NotNull PsiFile file) { super.visitFile(file); if (CHECK_FOR_FILES_CONTAINING_BOM) { - if (file.getViewProvider().getBaseLanguage() != file.getLanguage()) { - // don't warn multiple times on files which have multiple views like PHP and JSP - return; - } - VirtualFile virtualFile = file.getVirtualFile(); - byte[] bom = virtualFile == null ? null : virtualFile.getBOM(); - if (bom != null) { - String hex = IntStream.range(0, bom.length) - .map(i -> bom[i]) - .mapToObj(b -> StringUtil.toUpperCase(Integer.toString(b & 0x00ff, 16))) - .collect(Collectors.joining()); - Charset charsetFromBOM = CharsetToolkit.guessFromBOM(bom); - final String signature = charsetFromBOM == null - ? "" - : LangBundle.message("inspection.message.charset.signature", charsetFromBOM.displayName()); - holder.registerProblem(file, LangBundle.message("inspection.message.file.contains.bom", hex, signature)); - } + checkBOM(file, holder); } } }; } + // null means natural range + private static TextRange getCommentRange(@NotNull PsiElement comment, @NotNull String text) { + Language language = comment.getLanguage(); + Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(language); + if (commenter == null) { + return null; + } + for (String prefix : commenter.getLineCommentPrefixes()) { + if (StringUtil.startsWith(text, prefix)) { + return new TextRange(prefix.length(), text.length()); + } + } + String blockCommentPrefix = commenter.getBlockCommentPrefix(); + if (blockCommentPrefix != null && StringUtil.startsWith(text, blockCommentPrefix)) { + String suffix = commenter.getBlockCommentSuffix(); + int endOffset = text.length() - (suffix != null && StringUtil.endsWith(text, blockCommentPrefix.length(), text.length(), suffix) ? suffix.length() : 0); + return new TextRange(blockCommentPrefix.length(), endOffset); + } + return null; + } + + private static void checkBOM(@NotNull PsiFile file, @NotNull ProblemsHolder holder) { + if (file.getViewProvider().getBaseLanguage() != file.getLanguage()) { + // don't warn multiple times on files which have multiple views like PHP and JSP + return; + } + VirtualFile virtualFile = file.getVirtualFile(); + byte[] bom = virtualFile == null ? null : virtualFile.getBOM(); + if (bom != null) { + String hex = IntStream.range(0, bom.length) + .map(i -> bom[i]) + .mapToObj(b -> StringUtil.toUpperCase(Integer.toString(b & 0x00ff, 16))) + .collect(Collectors.joining()); + Charset charsetFromBOM = CharsetToolkit.guessFromBOM(bom); + final String signature = charsetFromBOM == null + ? "" + : CodeInsightBundle.message("non.ascii.chars.inspection.message.charset.signature", charsetFromBOM.displayName()); + holder.registerProblem(file, CodeInsightBundle.message("non.ascii.chars.inspection.message.file.contains.bom", hex, signature)); + } + } + + // if element is an identifier, return its text (its non-trivial in case of Groovy) + private static boolean isIdentifier(@NotNull PsiElement element) { + if (element instanceof ForeignLeafPsiElement) return false; + PsiElement parent = element.getParent(); + PsiElement identifier; + if (parent instanceof PsiNameIdentifierOwner && + (identifier = ((PsiNameIdentifierOwner)parent).getNameIdentifier()) != null) { + // Groovy has this twisted PSI where method.getNameIdentifier() is some random light element + String text = element.getText(); + return identifier == element || text.equals(identifier.getText()); + } + // or it maybe the reference name + if (parent instanceof PsiReference) { + PsiElement refElement = ((PsiReference)parent).getElement(); + return refElement == parent || refElement == element; + } + return false; + } + private static boolean isFileWorthIt(@NotNull PsiFile file) { if (InjectedLanguageManager.getInstance(file.getProject()).isInjectedFragment(file)) return false; VirtualFile virtualFile = file.getVirtualFile(); @@ -126,38 +201,91 @@ public class NonAsciiCharactersInspection extends LocalInspectionTool { return !(charset instanceof Native2AsciiCharset); } - - private static void checkSameLanguage(@NotNull PsiElement element, @NotNull String text, @NotNull ProblemsHolder holder) { - Set scripts = text.codePoints() - .mapToObj(Character.UnicodeScript::of) - .filter(script -> !script.equals(Character.UnicodeScript.COMMON)) - .collect(Collectors.toSet()); - if (scripts.size() > 1) { - List list = new ArrayList<>(scripts); - Collections.sort(list); // a little bit of stability - holder.registerProblem(element, LangBundle.message("inspection.message.symbols.from.different.languages.found", list), - ProblemHighlightType.GENERIC_ERROR_OR_WARNING); + private static void reportMixedLanguages(@NotNull PsiElement element, + @NotNull String text, + @NotNull ProblemsHolder holder, + @Nullable("null means natural range") TextRange elementRange) { + Character.UnicodeScript first = null; + Character.UnicodeScript second = null; + int i; + int codePoint = -1; + int endOffset = elementRange == null ? text.length() : elementRange.getEndOffset(); + int startOffset = elementRange == null ? 0 : elementRange.getStartOffset(); + for (i = startOffset; i < endOffset; i++) { + codePoint = text.codePointAt(i); + Character.UnicodeScript currentScript = Character.UnicodeScript.of(codePoint); + if (ignoreScript(currentScript)) { + if (i == startOffset) startOffset++; + continue; // ignore '123.(&$'... + } + second = currentScript; + if (first == null) { + first = second; + } + else if (first != second) { + break; + } + // advance to the next codepoint + if (Character.isLowSurrogate((char)codePoint)) { + i++; + } } + if (first == null || first == second) { + return; + } + // found two scripts + // now [startOffset..i) are of 'first' script + int j; + for (j = Character.isLowSurrogate((char)codePoint) ? i + 1 : i; j < endOffset; j++) { + codePoint = text.codePointAt(j); + Character.UnicodeScript currentScript = Character.UnicodeScript.of(codePoint); + if (ignoreScript(currentScript)) continue; + if (currentScript != second) { + break; + } + // advance to the next codepoint + if (Character.isLowSurrogate((char)codePoint)) { + j++; + } + } + // ignore trailing COMMON script characters + for (; j > i; j--) { + codePoint = text.codePointAt(j-1); + if (!ignoreScript(Character.UnicodeScript.of(codePoint))) break; + } + // now [i..j) are of 'second' script + // try to report the range which is the least latin + TextRange toReport; + if (first == Character.UnicodeScript.LATIN) { + toReport = new TextRange(i, j); + } + else { + toReport = new TextRange(startOffset, i); + Character.UnicodeScript t = second; + second = first; + first = t; + } + holder.registerProblem(element, toReport, CodeInsightBundle.message("non.ascii.chars.inspection.message.symbols.from.different.languages.found", second, first)); } - private static void checkAscii(@NotNull PsiElement element, @NotNull String text, @NotNull ProblemsHolder holder) { - if (!IOUtil.isAscii(text)) { - holder.registerProblem(element, LangBundle.message("inspection.message.non.ascii.characters.in", Kind.IDENTIFIER.ordinal())); - } + private static boolean ignoreScript(@NotNull Character.UnicodeScript script) { + return script == Character.UnicodeScript.COMMON || script == Character.UnicodeScript.INHERITED; } - private static void checkAsciiRange(@NotNull PsiElement element, - @NotNull String text, - @NotNull ProblemsHolder holder, - @NotNull Kind kind) { + private static void reportNonAsciiRange(@NotNull PsiElement element, + @NotNull String text, + @NotNull ProblemsHolder holder, + @Nullable("null means natural range") TextRange elementRange) { int errorCount = 0; int start = -1; - for (int i = 0; i <= text.length(); i++) { - char c = i >= text.length() ? 0 : text.charAt(i); - if (i == text.length() || c<128) { + int startOffset = elementRange == null ? 0 : elementRange.getStartOffset(); + int endOffset = elementRange == null ? text.length() : elementRange.getEndOffset(); + for (int i = startOffset; i <= endOffset; i++) { + char c = i >= endOffset ? 0 : text.charAt(i); + if (i == endOffset || c < 128) { if (start != -1) { TextRange range = new TextRange(start, i); - holder.registerProblem(element, range, LangBundle.message("inspection.message.non.ascii.characters.in", kind.ordinal())); + holder.registerProblem(element, range, CodeInsightBundle.message("non.ascii.chars.inspection.message.non.ascii.characters")); start = -1; //do not report too many errors if (errorCount++ > 200) break; @@ -175,7 +303,19 @@ public class NonAsciiCharactersInspection extends LocalInspectionTool { return new NonAsciiCharactersInspectionFormUi(this).getPanel(); } - private enum Kind { - IDENTIFIER, COMMENT, LITERAL + enum PsiElementKind { IDENTIFIER, STRING, COMMENT, OTHER} + @NotNull + private static PsiElementKind getKind(@NotNull PsiElement element, SyntaxHighlighter syntaxHighlighter) { + TextAttributesKey[] keys; + if (element.getParent() instanceof PsiLiteralValue || ChunkExtractor.isHighlightedAsString(keys = syntaxHighlighter.getTokenHighlights(((LeafPsiElement)element).getElementType()))) { + return PsiElementKind.STRING; + } + if (isIdentifier(element)) { + return PsiElementKind.IDENTIFIER; + } + if (element instanceof PsiComment || ChunkExtractor.isHighlightedAsComment(keys)) { + return PsiElementKind.COMMENT; + } + return PsiElementKind.OTHER; } } diff --git a/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspectionFormUi.kt b/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspectionFormUi.kt index 226e558fd27f..643639ee95ae 100644 --- a/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspectionFormUi.kt +++ b/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspectionFormUi.kt @@ -7,67 +7,101 @@ import com.intellij.util.ui.CheckBox class NonAsciiCharactersInspectionFormUi(entry: InspectionProfileEntry) { - val panel = panel { - buttonsGroup(CodeInsightBundle.message("non.ascii.warn.of")) { + row { + cell( + CheckBox( + CodeInsightBundle.message("non.ascii.chars.inspection.option.files.containing.bom.checkbox"), + entry, + "CHECK_FOR_FILES_CONTAINING_BOM" + ) + ) + } + buttonsGroup(CodeInsightBundle.message("non.ascii.chars.inspection.non.ascii.top.label")) { row { cell( CheckBox( - CodeInsightBundle.message("checkbox.non.ascii.option.characters.in.identifiers"), + CodeInsightBundle.message("non.ascii.chars.inspection.option.characters.in.identifiers.checkbox"), entry, "CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME" ) - ).comment(CodeInsightBundle.message("label.non.ascii.chars.example.characters.in.identifiers")) + ) + comment(CodeInsightBundle.message("non.ascii.chars.inspection.example.characters.in.identifiers.label")) } row { cell( CheckBox( - CodeInsightBundle.message("checkbox.non.ascii.option.characters.in.comments"), + CodeInsightBundle.message("non.ascii.chars.inspection.option.characters.in.strings.checkbox"), entry, "CHECK_FOR_NOT_ASCII_STRING_LITERAL" ) - ).comment(CodeInsightBundle.message("label.non.ascii.chars.example.characters.in.comments")) + ) + comment(CodeInsightBundle.message("non.ascii.chars.inspection.example.characters.in.strings.label")) } row { cell( CheckBox( - CodeInsightBundle.message("checkbox.non.ascii.option.characters.in.strings"), + CodeInsightBundle.message("non.ascii.chars.inspection.option.characters.in.comments.checkbox"), entry, "CHECK_FOR_NOT_ASCII_COMMENT" ) - ).comment(CodeInsightBundle.message("label.non.ascii.chars.example.characters.in.strings")) + ) + comment(CodeInsightBundle.message("non.ascii.chars.inspection.example.characters.in.comments.label")) } row { cell( CheckBox( - CodeInsightBundle.message("checkbox.non.ascii.option.different.languages.in.identifiers"), + CodeInsightBundle.message("non.ascii.chars.inspection.option.characters.in.any.other.word.checkbox"), + entry, + "CHECK_FOR_NOT_ASCII_IN_ANY_OTHER_WORD" + ) + ) + comment(CodeInsightBundle.message("non.ascii.chars.inspection.example.characters.in.any.other.word.label")) + } + } + buttonsGroup(CodeInsightBundle.message("non.ascii.chars.inspection.mixed.chars.top.label")) { + row { + cell( + CheckBox( + CodeInsightBundle.message("non.ascii.chars.inspection.option.mixed.languages.in.identifiers.checkbox"), entry, "CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME" ) - ).comment(CodeInsightBundle.message("label.non.ascii.chars.example.different.languages.in.identifiers")) + ) + comment(CodeInsightBundle.message("non.ascii.chars.inspection.example.mixed.languages.in.identifiers.label")) } - row { cell( CheckBox( - CodeInsightBundle.message("checkbox.non.ascii.option.different.languages.in.string"), + CodeInsightBundle.message("non.ascii.chars.inspection.option.mixed.languages.in.strings.checkbox"), entry, "CHECK_FOR_DIFFERENT_LANGUAGES_IN_STRING" ) - ).comment(CodeInsightBundle.message("label.non.ascii.chars.example.different.languages.in.string")) + ) + comment(CodeInsightBundle.message("non.ascii.chars.inspection.example.mixed.languages.in.string.label")) } - row { cell( CheckBox( - CodeInsightBundle.message("checkbox.non.ascii.option.files.containing.bom"), - entry, - "CHECK_FOR_FILES_CONTAINING_BOM" - ) - ).comment(CodeInsightBundle.message("label.non.ascii.chars.example.files.containing.bom")) + CodeInsightBundle.message("non.ascii.chars.inspection.option.mixed.languages.in.comments.checkbox"), + entry, + "CHECK_FOR_DIFFERENT_LANGUAGES_IN_COMMENTS" + ) + ) + comment(CodeInsightBundle.message("non.ascii.chars.inspection.example.mixed.languages.in.comments.label")) + } + row { + cell( + CheckBox( + CodeInsightBundle.message("non.ascii.chars.inspection.option.mixed.languages.in.any.other.word.checkbox"), + entry, + "CHECK_FOR_DIFFERENT_LANGUAGES_IN_ANY_OTHER_WORD" + ) + ) + comment(CodeInsightBundle.message("non.ascii.chars.inspection.example.mixed.languages.in.any.other.word.label")) } } }