diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/Simple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/Simple.java new file mode 100644 index 000000000000..42c434cce987 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters/Simple.java @@ -0,0 +1,9 @@ +class X { + int Ж = 0; + class InnerП {} + // commentжп 234 + String s = "12л3орыва0"; + void жжж() { + жжж(); + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/NonAsciiCharactersTest.java b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/NonAsciiCharactersTest.java new file mode 100644 index 000000000000..6e2afec75430 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/NonAsciiCharactersTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2017 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. + */ + +/* + * Created by IntelliJ IDEA. + * User: max + * Date: Apr 11, 2002 + * Time: 7:51:16 PM + * To change template for new class use + * Code Style | Class Templates options (Tools | IDE Options). + */ +package com.intellij.codeInsight.daemon; + +import com.intellij.codeInspection.NonAsciiCharactersInspection; +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +public class NonAsciiCharactersTest extends DaemonAnalyzerTestCase { + @NonNls private static final String BASE_PATH = "/codeInsight/daemonCodeAnalyzer/nonAsciiCharacters"; + + @NotNull + @Override + protected LocalInspectionTool[] configureLocalInspectionTools() { + NonAsciiCharactersInspection inspection = new NonAsciiCharactersInspection(); + inspection.CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME = 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}; + } + + private void doTest() throws Exception { + doTest(BASE_PATH + "/" + getTestName(false)+".java", true, false); + UIUtil.dispatchAllInvocationEvents(); + } + + public void testSimple() throws Exception { + doTest(); + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspection.form b/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspection.form new file mode 100644 index 000000000000..7572dd0db29c --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspection.form @@ -0,0 +1,111 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspection.java b/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspection.java new file mode 100644 index 000000000000..4530712f10cb --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInspection/NonAsciiCharactersInspection.java @@ -0,0 +1,228 @@ +/* + * Copyright 2000-2017 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 com.intellij.codeInspection; + +import com.intellij.lang.injection.InjectedLanguageManager; +import com.intellij.lang.properties.charset.Native2AsciiCharset; +import com.intellij.openapi.fileEditor.impl.LoadTextUtil; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vfs.CharsetToolkit; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.ui.components.JBCheckBox; +import com.intellij.util.ReflectionUtil; +import com.intellij.util.io.IOUtil; +import gnu.trove.THashMap; +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.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +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_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME = true; + public boolean CHECK_FOR_FILES_CONTAINING_BOM; + + private JBCheckBox myASCIIIdentifiers; + private JBCheckBox myASCIIComments; + private JBCheckBox myASCIIStringLiterals; + private JBCheckBox myAlienIdentifiers; + private JPanel myPanel; + private JBCheckBox myFilesContainingBOM; + private final Map myBindings = new THashMap<>(); + + public NonAsciiCharactersInspection() { + bind(myASCIIIdentifiers, "CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME"); + bind(myASCIIStringLiterals, "CHECK_FOR_NOT_ASCII_STRING_LITERAL"); + bind(myASCIIComments, "CHECK_FOR_NOT_ASCII_COMMENT"); + bind(myAlienIdentifiers, "CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME"); + bind(myFilesContainingBOM, "CHECK_FOR_FILES_CONTAINING_BOM"); + } + + @Override + @Nls + @NotNull + public String getGroupDisplayName() { + return InspectionsBundle.message("group.names.internationalization.issues"); + } + + @Override + @Nls + @NotNull + public String getDisplayName() { + return InspectionsBundle.message("non.ascii.characters"); + } + + @Override + @NonNls + @NotNull + public String getShortName() { + return "NonAsciiCharacters"; + } + + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, + boolean isOnTheFly, + @NotNull LocalInspectionToolSession session) { + if (!isFileWorthIt(session.getFile())) return PsiElementVisitor.EMPTY_VISITOR; + return new PsiElementVisitor() { + @Override + public void visitElement(PsiElement element) { + if (CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME || CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME) { + PsiElement parent = element.getParent(); + if (parent instanceof PsiNameIdentifierOwner && ((PsiNameIdentifierOwner)parent).getNameIdentifier() == element) { + String text = element.getText(); + if (CHECK_FOR_NOT_ASCII_IDENTIFIER_NAME) { + checkAscii(element, text, holder, "an identifier"); + } + if (CHECK_FOR_DIFFERENT_LANGUAGES_IN_IDENTIFIER_NAME) { + checkSameLanguage(element, text, holder); + } + } + } + if (CHECK_FOR_NOT_ASCII_COMMENT) { + if (element instanceof PsiComment) { + checkAsciiRange(element, element.getText(), holder, "a comment"); + } + } + if (CHECK_FOR_NOT_ASCII_STRING_LITERAL) { + if (element instanceof PsiLiteralValue) { + checkAsciiRange(element, element.getText(), holder, "a string literal"); + } + } + } + + @Override + public void visitFile(PsiFile file) { + super.visitFile(file); + if (CHECK_FOR_FILES_CONTAINING_BOM) { + 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 -> Integer.toString(b & 0x00ff, 16).toUpperCase()) + .collect(Collectors.joining()); + Charset charsetFromBOM = CharsetToolkit.guessFromBOM(bom); + holder.registerProblem(file, "File contains BOM: '" + hex +"'"+ + (charsetFromBOM == null ? "" : " (charset '"+charsetFromBOM.displayName()+"' signature)"), + ProblemHighlightType.GENERIC_ERROR_OR_WARNING); + } + } + } + }; + } + + private static boolean isFileWorthIt(PsiFile file) { + if (InjectedLanguageManager.getInstance(file.getProject()).isInjectedFragment(file)) return false; + VirtualFile virtualFile = file.getVirtualFile(); + if (virtualFile == null) return false; + CharSequence text = file.getViewProvider().getContents(); + + Charset charset = LoadTextUtil.extractCharsetFromFileContent(file.getProject(), virtualFile, text); + + // no sense in checking transparently decoded file: all characters there are already safely encoded + return !(charset instanceof Native2AsciiCharset); + } + + + private static void checkSameLanguage(PsiElement element, + String text, + 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, "Identifier contains symbols from different languages: " + list, + ProblemHighlightType.GENERIC_ERROR_OR_WARNING); + } + } + + private static void checkAscii(PsiElement element, + String text, + ProblemsHolder holder, + String where) { + if (!IOUtil.isAscii(text)) { + holder.registerProblem(element, "Non-ASCII characters in " + where, ProblemHighlightType.GENERIC_ERROR_OR_WARNING); + } + } + private static void checkAsciiRange(PsiElement element, + String text, + ProblemsHolder holder, + String where) { + 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) { + if (start != -1) { + TextRange range = new TextRange(start, i); + String message = "Non-ASCII characters in " + where; + holder.registerProblem(element, range, message); + start = -1; + //do not report too many errors + if (errorCount++ > 200) break; + } + } + else if (start == -1) { + start = i; + } + } + } + + @Nullable + @Override + public JComponent createOptionsPanel() { + reset(); + return myPanel; + } + + private void bind(JCheckBox checkBox, String property) { + myBindings.put(checkBox, property); + reset(checkBox, property); + checkBox.addChangeListener(__ -> { + boolean selected = checkBox.isSelected(); + ReflectionUtil.setField(getClass(), this, boolean.class, property, selected); + }); + } + + private void reset(JCheckBox checkBox, String property) { + checkBox.setSelected(ReflectionUtil.getField(getClass(), this, boolean.class, property)); + } + + private void reset() { + for (Map.Entry entry : myBindings.entrySet()) { + JCheckBox checkBox = entry.getKey(); + String property = entry.getValue(); + reset(checkBox, property); + } + } +} diff --git a/platform/platform-resources-en/src/inspectionDescriptions/NonAsciiCharacters.html b/platform/platform-resources-en/src/inspectionDescriptions/NonAsciiCharacters.html new file mode 100644 index 000000000000..07c70937380c --- /dev/null +++ b/platform/platform-resources-en/src/inspectionDescriptions/NonAsciiCharacters.html @@ -0,0 +1,18 @@ + + + +This inspection warns you of the code accidentally written in the wrong encoding.
+ +For example,
+
    +
  • Non-ASCII characters used in the names/strings/comments or
  • +
  • Identifiers written in different languages + (e.g. when in the variable "myCollection" + the letter C written in Cyrillic) or
  • +
  • Unicode symbols used in comments or strings + (e.g. long dashes and arrows in the comment + "// a → !a — please note")
  • +
+ + + \ No newline at end of file diff --git a/platform/platform-resources-en/src/messages/InspectionsBundle.properties b/platform/platform-resources-en/src/messages/InspectionsBundle.properties index 2a4f64eb0955..cbc25d9bf284 100644 --- a/platform/platform-resources-en/src/messages/InspectionsBundle.properties +++ b/platform/platform-resources-en/src/messages/InspectionsBundle.properties @@ -714,6 +714,7 @@ cleanup.in.scope=Cleanup code on... severities.default.settings.message=Edit Settings|Colors \\& Fonts unsupported.character.for.the.charset=Unsupported characters for the charset ''{0}'' lossy.encoding=Lossy encoding +non.ascii.characters=Non-ASCII characters inspection.duplicated.code.display.name=Duplicated Code inconsistent.line.separators=Inconsistent line separators inspection.unused.symbol.public.method.parameters=Check parameters in &Non-private methods diff --git a/platform/platform-resources/src/META-INF/LangExtensions.xml b/platform/platform-resources/src/META-INF/LangExtensions.xml index 9af16133aea8..73c200780741 100644 --- a/platform/platform-resources/src/META-INF/LangExtensions.xml +++ b/platform/platform-resources/src/META-INF/LangExtensions.xml @@ -831,6 +831,9 @@ +