From 9aac209c7260e9b4eba831eb530075d888a1ccd1 Mon Sep 17 00:00:00 2001 From: Aleksey Dobrynin Date: Mon, 7 Oct 2024 23:13:39 +0200 Subject: [PATCH] [properties, inspection, fix] non-ISO 8859-1 characters for java 1.8 and below IDEA-360007 GitOrigin-RevId: 7a45cac5966b251a5db6759c16f3709392c5a3fc --- .../messages/JavaPsiBundle.properties | 1 + .../com/intellij/pom/java/JavaFeature.java | 1 + .../UnsupportedCharacter.html | 19 ++ platform/core-api/api-dump-unreviewed.txt | 1 + .../openapi/fileTypes/CharsetUtil.java | 92 ++++++++- .../LossyEncodingInspection.java | 82 +------- .../java-i18n/resources/META-INF/plugin.xml | 5 + .../messages/JavaI18nBundle.properties | 3 + .../UnsupportedCharacterInspection.java | 102 ++++++++++ .../UnsupportedCharacterInspectionTest.java | 186 ++++++++++++++++++ .../intellij.properties.psi.iml | 2 +- 11 files changed, 414 insertions(+), 80 deletions(-) create mode 100644 java/java-impl/src/inspectionDescriptions/UnsupportedCharacter.html create mode 100644 plugins/java-i18n/src/com/intellij/lang/properties/UnsupportedCharacterInspection.java create mode 100644 plugins/java-i18n/testSrc/com/intellij/codeInspection/i18n/UnsupportedCharacterInspectionTest.java diff --git a/java/java-frontback-psi-api/resources/messages/JavaPsiBundle.properties b/java/java-frontback-psi-api/resources/messages/JavaPsiBundle.properties index 0727b07ea548..a7cf6d9d2a01 100644 --- a/java/java-frontback-psi-api/resources/messages/JavaPsiBundle.properties +++ b/java/java-frontback-psi-api/resources/messages/JavaPsiBundle.properties @@ -84,6 +84,7 @@ feature.effectively.final=Effectively final variables feature.try.with.resources.refs=Resource references feature.modules=Modules feature.private.interface.methods=Private interface methods +feature.utf8.property.files=Property files in UTF-8 encoding feature.collection.factories=Collection factory methods feature.lvti=Local variable type inference feature.var.lambda.parameter='var' in lambda parameters diff --git a/java/java-frontback-psi-api/src/com/intellij/pom/java/JavaFeature.java b/java/java-frontback-psi-api/src/com/intellij/pom/java/JavaFeature.java index 868e270922b9..70d812faee35 100644 --- a/java/java-frontback-psi-api/src/com/intellij/pom/java/JavaFeature.java +++ b/java/java-frontback-psi-api/src/com/intellij/pom/java/JavaFeature.java @@ -51,6 +51,7 @@ public enum JavaFeature { MODULES(LanguageLevel.JDK_1_9, "feature.modules"), COLLECTION_FACTORIES(LanguageLevel.JDK_1_9, "feature.collection.factories"), PRIVATE_INTERFACE_METHODS(LanguageLevel.JDK_1_9, "feature.private.interface.methods"), + UTF8_PROPERTY_FILES(LanguageLevel.JDK_1_9, "feature.utf8.property.files"), LVTI(LanguageLevel.JDK_10, "feature.lvti"), VAR_LAMBDA_PARAMETER(LanguageLevel.JDK_11, "feature.var.lambda.parameter"), NESTMATES(LanguageLevel.JDK_11, "feature.nestmates"), diff --git a/java/java-impl/src/inspectionDescriptions/UnsupportedCharacter.html b/java/java-impl/src/inspectionDescriptions/UnsupportedCharacter.html new file mode 100644 index 000000000000..5b7eb1c02c56 --- /dev/null +++ b/java/java-impl/src/inspectionDescriptions/UnsupportedCharacter.html @@ -0,0 +1,19 @@ + + +Reports characters in property files that are unsupported by the ISO-8859-1 charset when Java 1.8 or earlier is used. +The inspection checks if a property file contains characters that cannot be mapped to ISO-8859-1 and suggests converting them to Unicode escape sequences for compatibility. +

Configure the inspection:

+

+ Use the Convert to escape sequences quick-fix to ensure compatibility with ISO-8859-1 encoding: +

+ +

Example:

+

+  key=Java + ☕ = 🍀
+
+

After the quick-fix is applied:

+

+  key=Java + \u2615 = \uD83C\uDF40
+
+ + diff --git a/platform/core-api/api-dump-unreviewed.txt b/platform/core-api/api-dump-unreviewed.txt index 98ea1598ea0e..b93455471907 100644 --- a/platform/core-api/api-dump-unreviewed.txt +++ b/platform/core-api/api-dump-unreviewed.txt @@ -1895,6 +1895,7 @@ f:com.intellij.openapi.fileTypes.BinaryFileTypeDecompilers f:com.intellij.openapi.fileTypes.CharsetUtil - ():V - s:extractCharsetFromFileContent(com.intellij.openapi.project.Project,com.intellij.openapi.vfs.VirtualFile,com.intellij.openapi.fileTypes.FileType,java.lang.CharSequence):java.nio.charset.Charset +- s:findUnmappableCharacters(java.lang.CharSequence,java.nio.charset.Charset):com.intellij.openapi.util.TextRange com.intellij.openapi.fileTypes.DirectoryFileType - com.intellij.openapi.fileTypes.FileType com.intellij.openapi.fileTypes.FileType diff --git a/platform/core-api/src/com/intellij/openapi/fileTypes/CharsetUtil.java b/platform/core-api/src/com/intellij/openapi/fileTypes/CharsetUtil.java index d27c0362dd16..dc2589f7c854 100644 --- a/platform/core-api/src/com/intellij/openapi/fileTypes/CharsetUtil.java +++ b/platform/core-api/src/com/intellij/openapi/fileTypes/CharsetUtil.java @@ -1,13 +1,18 @@ -// Copyright 2000-2020 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-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.openapi.fileTypes; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ReflectionUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.nio.charset.Charset; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.*; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -34,4 +39,87 @@ public final class CharsetUtil { } return null; } + + /** + * Checks if the given text contains characters that cannot be mapped to the specified charset during encoding. + * + * @param text the character sequence to be checked + * @param charset the charset to be used for encoding + * @return a {@code TextRange} representing the range of unmappable characters, or {@code null} if all characters can be mapped + */ + public static @Nullable TextRange findUnmappableCharacters(@Nullable CharSequence text, @NotNull Charset charset) { + if(text == null || text.length() == 0) return null; + return findUnmappableRange(CharBuffer.wrap(text), Ref.create(), CharBuffer.allocate(text.length()), charset); + } + + /** + * Identifies the range of characters that either fail to encode or decode properly with the specified charset. + * + * @param inputBuffer the input character buffer to be checked + * @param encodedBufferRef a reference to the output byte buffer for storing encoded bytes + * @param decodedBuffer a character buffer to hold the decoded characters + * @param charset the charset used for encoding and decoding + * @return a {@code TextRange} object representing the range of unmappable characters, or {@code null} if all characters are mappable + */ + private static @Nullable TextRange findUnmappableRange(@NotNull CharBuffer inputBuffer, + @NotNull Ref encodedBufferRef, + @NotNull CharBuffer decodedBuffer, + @NotNull Charset charset) { + CharsetEncoder encoder = charset.newEncoder() + .onUnmappableCharacter(CodingErrorAction.REPORT) + .onMalformedInput(CodingErrorAction.REPORT); + int remainingChars = inputBuffer.limit(); + + ByteBuffer encodedBuffer = encodedBufferRef.get(); + if (encodedBuffer == null) { + encodedBufferRef.set(encodedBuffer = ByteBuffer.allocate((int)(encoder.averageBytesPerChar() * remainingChars))); + } + encodedBuffer.rewind(); + encodedBuffer.limit(encodedBuffer.capacity()); + inputBuffer.rewind(); + inputBuffer.position(0); + CoderResult encodeResult; + + while (true) { + encodeResult = inputBuffer.hasRemaining() ? encoder.encode(inputBuffer, encodedBuffer, true) : CoderResult.UNDERFLOW; + if (encodeResult.isUnderflow()) { + encodeResult = encoder.flush(encodedBuffer); + } + if (!encodeResult.isOverflow()) { + break; + } + + ByteBuffer tempBuffer = ByteBuffer.allocate(3 * encodedBuffer.capacity() / 2 + 1); + encodedBuffer.flip(); + tempBuffer.put(encodedBuffer); + encodedBufferRef.set(encodedBuffer = tempBuffer); + } + + if (encodeResult.isError()) { + return TextRange.from(inputBuffer.position(), encodeResult.length()); + } + + int encodedLength = encodedBuffer.position(); + CharsetDecoder decoder = charset.newDecoder() + .onUnmappableCharacter(CodingErrorAction.REPORT) + .onMalformedInput(CodingErrorAction.REPORT); + encodedBuffer.rewind(); + encodedBuffer.limit(encodedLength); + decodedBuffer.rewind(); + + CoderResult decodeResult = decoder.decode(encodedBuffer, decodedBuffer, true); + if (decodeResult.isError()) { + return TextRange.from(decodedBuffer.position(), decodeResult.length()); + } + + if (decodedBuffer.position() != remainingChars) { + return TextRange.from(Math.min(remainingChars, decodedBuffer.position()), 1); + } + + inputBuffer.rewind(); + inputBuffer.position(0); + decodedBuffer.rewind(); + int commonPrefixLength = StringUtil.commonPrefixLength(inputBuffer, decodedBuffer); + return commonPrefixLength == remainingChars ? null : TextRange.from(commonPrefixLength, 1); + } } diff --git a/platform/lang-impl/src/com/intellij/codeInspection/LossyEncodingInspection.java b/platform/lang-impl/src/com/intellij/codeInspection/LossyEncodingInspection.java index fea782c47d89..b7bbb63e81eb 100644 --- a/platform/lang-impl/src/com/intellij/codeInspection/LossyEncodingInspection.java +++ b/platform/lang-impl/src/com/intellij/codeInspection/LossyEncodingInspection.java @@ -12,11 +12,10 @@ import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.impl.LoadTextUtil; +import com.intellij.openapi.fileTypes.CharsetUtil; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.ListPopup; -import com.intellij.openapi.util.Ref; 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.openapi.vfs.encoding.ChangeFileEncodingAction; @@ -31,9 +30,7 @@ import com.intellij.util.SmartList; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.*; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.*; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -173,17 +170,12 @@ public final class LossyEncodingInspection extends LocalInspectionTool { @NotNull CharSequence text, @NotNull Charset charset, @NotNull List descriptors) { - CharBuffer buffer = CharBuffer.wrap(text); - - int textLength = text.length(); - CharBuffer back = CharBuffer.allocate(textLength); // must be enough, error otherwise - - Ref outRef = Ref.create(); - //do not report too many errors for (int pos = 0, errorCount = 0; pos < text.length() && errorCount < 200; errorCount++) { - TextRange errRange = nextUnmappable(buffer, pos, outRef, back, charset); + TextRange errRange = CharsetUtil.findUnmappableCharacters(text.subSequence(pos, text.length()), charset); if (errRange == null) break; + errRange = errRange.shiftRight(pos); + ProblemDescriptor lastDescriptor = ContainerUtil.getLastItem(descriptors); if (lastDescriptor != null && lastDescriptor.getTextRangeInElement().getEndOffset() == errRange.getStartOffset()) { // combine two adjacent descriptors @@ -199,70 +191,6 @@ public final class LossyEncodingInspection extends LocalInspectionTool { } } - // returns null if OK - // range of the characters either failed to be encoded to bytes or failed to be decoded back or decoded to the chars different from the original - private static TextRange nextUnmappable(@NotNull CharBuffer in, - int position, - @NotNull Ref outRef, - @NotNull CharBuffer back, - @NotNull Charset charset) { - CharsetEncoder encoder = charset.newEncoder() - .onUnmappableCharacter(CodingErrorAction.REPORT) - .onMalformedInput(CodingErrorAction.REPORT); - int textLength = in.limit() - position; - - ByteBuffer out = outRef.get(); - if (out == null) { - outRef.set(out = ByteBuffer.allocate((int)(encoder.averageBytesPerChar() * textLength))); - } - out.rewind(); - out.limit(out.capacity()); - in.rewind(); - in.position(position); - CoderResult cr; - for (;;) { - cr = in.hasRemaining() ? encoder.encode(in, out, true) : CoderResult.UNDERFLOW; - if (cr.isUnderflow()) { - cr = encoder.flush(out); - } - - if (!cr.isOverflow()) { - break; - } - - int n = 3 * out.capacity()/2 + 1; - ByteBuffer tmp = ByteBuffer.allocate(n); - out.flip(); - tmp.put(out); - outRef.set(out = tmp); - } - if (cr.isError()) { - return TextRange.from(in.position(), cr.length()); - } - // phew, encoded successfully. now check if we can decode it back with char-to-char precision - int outLength = out.position(); - CharsetDecoder decoder = charset.newDecoder() - .onUnmappableCharacter(CodingErrorAction.REPORT) - .onMalformedInput(CodingErrorAction.REPORT); - out.rewind(); - out.limit(outLength); - back.rewind(); - CoderResult dr = decoder.decode(out, back, true); - if (dr.isError()) { - return TextRange.from(back.position(), dr.length()); - } - if (back.position() != textLength) { - return TextRange.from(Math.min(textLength, back.position()), 1); - } - // ok, we decoded it back to string. now compare if the strings are identical - in.rewind(); - in.position(position); - back.rewind(); - int len = StringUtil.commonPrefixLength(in, back); - if (len == textLength) return null; - return TextRange.from(len, 1); // let's report only the first diff char - } - private static final class ReloadInAnotherEncodingFix extends ChangeEncodingFix { ReloadInAnotherEncodingFix(@NotNull PsiFile file) { super(file); diff --git a/plugins/java-i18n/resources/META-INF/plugin.xml b/plugins/java-i18n/resources/META-INF/plugin.xml index 69e43bd41622..91eec7759257 100644 --- a/plugins/java-i18n/resources/META-INF/plugin.xml +++ b/plugins/java-i18n/resources/META-INF/plugin.xml @@ -58,6 +58,11 @@ groupBundle="messages.PropertiesBundle" groupKey="properties.files.inspection.group.display.name" enabledByDefault="true" level="WARNING" implementationClass="com.intellij.lang.properties.UnusedMessageFormatParameterInspection"/> + #ref #loc +unsupported.character.display.name=Unsupported character +unsupported.character.problem.descriptor=Unsupported characters for the charset ''{0}'' +unsupported.character.inspection.fix.description=Convert to escape sequences intention.family.name.mark.as.nlssafe=Mark as @NlsSafe capitalization.kind.title=title capitalization.kind.sentence=sentence diff --git a/plugins/java-i18n/src/com/intellij/lang/properties/UnsupportedCharacterInspection.java b/plugins/java-i18n/src/com/intellij/lang/properties/UnsupportedCharacterInspection.java new file mode 100644 index 000000000000..aaab2ac0004c --- /dev/null +++ b/plugins/java-i18n/src/com/intellij/lang/properties/UnsupportedCharacterInspection.java @@ -0,0 +1,102 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.lang.properties; + +import com.intellij.codeInspection.LocalInspectionToolSession; +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.java.i18n.JavaI18nBundle; +import com.intellij.lang.properties.charset.Native2AsciiCharset; +import com.intellij.lang.properties.psi.Property; +import com.intellij.lang.properties.references.PropertyReference; +import com.intellij.modcommand.ModPsiUpdater; +import com.intellij.modcommand.PsiUpdateModCommandQuickFix; +import com.intellij.openapi.fileTypes.CharsetUtil; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.encoding.EncodingRegistry; +import com.intellij.pom.java.JavaFeature; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiReference; +import com.intellij.psi.util.PsiUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class UnsupportedCharacterInspection extends PropertiesInspectionBase { + private static final Charset OLD_JAVA_DEFAULT_CHARSET = StandardCharsets.ISO_8859_1; + private static final Charset NEW_JAVA_DEFAULT_CHARSET = StandardCharsets.UTF_8; + + @Override + public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, + boolean isOnTheFly, + @NotNull LocalInspectionToolSession session) { + return new PsiElementVisitor() { + @Override + public void visitElement(@NotNull PsiElement element) { + super.visitElement(element); + if (PsiUtil.isAvailable(JavaFeature.UTF8_PROPERTY_FILES, element)) return; + PsiReference[] references = element.getReferences(); + for (PsiReference reference : references) { + if (reference instanceof PropertyReference propertyReference) { + if (!(propertyReference.resolve() instanceof Property property)) continue; + if (elementHasError(element, property)) return; + } + } + } + + private boolean elementHasError(@NotNull PsiElement element, @Nullable Property property) { + if (property == null) return false; + PsiFile psiFile = property.getContainingFile(); + if (psiFile.getFileType() != PropertiesFileType.INSTANCE) return false; + VirtualFile file = psiFile.getVirtualFile(); + if (file == null) return false; + EncodingRegistry encoding = EncodingRegistry.getInstance(); + boolean isCustomized = encoding.getDefaultCharsetForPropertiesFiles(file) != null || + encoding.getEncoding(file, true) != NEW_JAVA_DEFAULT_CHARSET; + return !isCustomized && hasErrorCharacter(element, property.getValue()); + } + + private boolean hasErrorCharacter(@NotNull PsiElement element, @Nullable String value) { + if (value == null) return false; + TextRange error = CharsetUtil.findUnmappableCharacters(value, OLD_JAVA_DEFAULT_CHARSET); + if (error == null) return false; + holder.registerProblem(element, JavaI18nBundle.message("unsupported.character.problem.descriptor", + OLD_JAVA_DEFAULT_CHARSET.displayName()), + ProblemHighlightType.WEAK_WARNING, new EncodePropertyFix()); + return true; + } + }; + } + + private static final class EncodePropertyFix extends PsiUpdateModCommandQuickFix { + + @Override + protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) { + if (PsiUtil.isAvailable(JavaFeature.UTF8_PROPERTY_FILES, element)) return; + PsiReference[] references = element.getReferences(); + for (PsiReference reference : references) { + if (reference instanceof PropertyReference propertyReference && + propertyReference.resolve() instanceof Property property) { + String propertyValue = property.getValue(); + TextRange errorValue = CharsetUtil.findUnmappableCharacters(propertyValue, OLD_JAVA_DEFAULT_CHARSET); + if (errorValue != null) { + ByteBuffer encoded = Native2AsciiCharset.wrap(OLD_JAVA_DEFAULT_CHARSET).encode(propertyValue); + String newValue = OLD_JAVA_DEFAULT_CHARSET.decode(encoded).toString(); + updater.getWritable(property).setValue(newValue); + } + } + } + } + + @Override + public @NotNull String getFamilyName() { + return JavaI18nBundle.message("unsupported.character.inspection.fix.description"); + } + } +} diff --git a/plugins/java-i18n/testSrc/com/intellij/codeInspection/i18n/UnsupportedCharacterInspectionTest.java b/plugins/java-i18n/testSrc/com/intellij/codeInspection/i18n/UnsupportedCharacterInspectionTest.java new file mode 100644 index 000000000000..45f847f8cd10 --- /dev/null +++ b/plugins/java-i18n/testSrc/com/intellij/codeInspection/i18n/UnsupportedCharacterInspectionTest.java @@ -0,0 +1,186 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInspection.i18n; + +import com.intellij.lang.properties.UnsupportedCharacterInspection; +import com.intellij.openapi.roots.LanguageLevelProjectExtension; +import com.intellij.openapi.vfs.encoding.EncodingManager; +import com.intellij.pom.java.LanguageLevel; +import com.intellij.psi.PsiFile; +import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase; +import org.intellij.lang.annotations.Language; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class UnsupportedCharacterInspectionTest extends JavaCodeInsightFixtureTestCase { + @Override + protected void setUp() throws Exception { + super.setUp(); + myFixture.enableInspections(new UnsupportedCharacterInspection()); + myFixture.addClass(""" + package java.util; + public class ResourceBundle { + public static ResourceBundle getBundle(String baseName) { return null; } + public String getString(String key) { return null; } + } + """); + + } + + public void testJava8WithConversion() throws IOException { + javaVersion(LanguageLevel.JDK_1_8); + + PsiFile javaFile = addClass("Test.java", """ + import java.util.*; + public final class Test { + public static void main(String[] args) { + String value = ResourceBundle.getBundle("test").getString("key1"); + } + } + """); + + PsiFile props = addFile("test.properties", "key1=Java + ☕"); + fileEncoding(props, StandardCharsets.UTF_8); + propertiesEncoding(props, null); + + checkHighlighting(javaFile); + applyFix(javaFile, "Convert to escape sequences"); + checkFile(props, "key1=Java + \\u2615"); + } + + public void testJava8PlusConstantWithConversion() throws IOException { + javaVersion(LanguageLevel.JDK_1_8); + + PsiFile javaFile = addClass("Test.java", """ + import java.util.*; + public final class Test { + private static final String KEY = "key1"; + public static void main(String[] args) { + String value = ResourceBundle.getBundle("test").getString(KEY); + } + } + """); + + PsiFile props = addFile("test.properties", "key1=Java + ☕"); + fileEncoding(props, StandardCharsets.UTF_8); + propertiesEncoding(props, null); + + checkHighlighting(javaFile); + applyFix(javaFile, "Convert to escape sequences"); + checkFile(props, "key1=Java + \\u2615"); + } + + public void testJava9NoConversionNeeded() { + javaVersion(LanguageLevel.JDK_1_9); + + PsiFile javaFile = addClass("Test.java", """ + import java.util.*; + public final class Test { + public static void main(String[] args) { + String value = ResourceBundle.getBundle("test").getString("key1"); + } + } + """); + + PsiFile props = addFile("test.properties", "key1=Java + ☕"); + fileEncoding(props, StandardCharsets.UTF_8); + propertiesEncoding(props, null); + + checkHighlighting(javaFile); + } + + public void testJava8WithCustomizedEncoding() { + javaVersion(LanguageLevel.JDK_1_8); + + PsiFile javaFile = addClass("Test.java", """ + import java.util.*; + public final class Test { + public static void main(String[] args) { + String value = ResourceBundle.getBundle("test").getString("key1"); + } + } + """); + + PsiFile props = addFile("test.properties", "key1=Java + ☕"); + fileEncoding(props, StandardCharsets.US_ASCII); + propertiesEncoding(props, null); + + checkHighlighting(javaFile); + } + + public void testJava8WithCustomizedEncoding2() { + javaVersion(LanguageLevel.JDK_1_8); + + PsiFile javaFile = addClass("Test.java", """ + import java.util.*; + public final class Test { + public static void main(String[] args) { + String value = ResourceBundle.getBundle("test").getString("key1"); + } + } + """); + + PsiFile props = addFile("test.properties", "key1=Java + ☕"); + fileEncoding(props, StandardCharsets.UTF_8); + propertiesEncoding(props, StandardCharsets.UTF_8); + + checkHighlighting(javaFile); + } + + public void testJava8NoConversionNeeded() { + javaVersion(LanguageLevel.JDK_1_8); + + PsiFile javaFile = addClass("Test.java", """ + import java.util.*; + public final class Test { + public static void main(String[] args) { + String value = ResourceBundle.getBundle("test").getString("key1"); + } + } + """); + + PsiFile props = addFile("test.properties", "key1=Java + Coffee"); + fileEncoding(props, StandardCharsets.UTF_8); + propertiesEncoding(props, null); + + checkHighlighting(javaFile); + } + + private void javaVersion(LanguageLevel jdk) { + LanguageLevelProjectExtension.getInstance(getProject()).setLanguageLevel(jdk); + } + + private static void fileEncoding(PsiFile file, Charset charset) { + EncodingManager.getInstance().setEncoding(file.getVirtualFile(), charset); + } + + private static void propertiesEncoding(PsiFile file, Charset charset) { + EncodingManager.getInstance().setDefaultCharsetForPropertiesFiles(file.getVirtualFile(), charset); + } + + @SuppressWarnings("SameParameterValue") + private PsiFile addClass(String fileName, @Language("JAVA") String fileContent) { + return myFixture.configureByText(fileName, fileContent); + } + + @SuppressWarnings("SameParameterValue") + private PsiFile addFile(String fileName, String fileContent) { + return myFixture.configureByText(fileName, fileContent); + } + + private void checkHighlighting(PsiFile file) { + myFixture.testHighlighting(true, false, true, file.getVirtualFile()); + } + + @SuppressWarnings("SameParameterValue") + private void applyFix(PsiFile file, String fixName) { + myFixture.configureFromExistingVirtualFile(file.getVirtualFile()); + myFixture.launchAction(myFixture.findSingleIntention(fixName)); + } + + @SuppressWarnings("SameParameterValue") + private static void checkFile(PsiFile file, String expectedContent) throws IOException { + assertEquals(expectedContent, file.getText()); + } +} diff --git a/plugins/properties/properties-psi-api/intellij.properties.psi.iml b/plugins/properties/properties-psi-api/intellij.properties.psi.iml index 89ba8d2de8bd..513cfc5e0b85 100644 --- a/plugins/properties/properties-psi-api/intellij.properties.psi.iml +++ b/plugins/properties/properties-psi-api/intellij.properties.psi.iml @@ -16,7 +16,7 @@ - +