[properties, inspection, fix] non-ISO 8859-1 characters for java 1.8 and below IDEA-360007

GitOrigin-RevId: 7a45cac5966b251a5db6759c16f3709392c5a3fc
This commit is contained in:
Aleksey Dobrynin
2024-10-07 23:13:39 +02:00
committed by intellij-monorepo-bot
parent 429a0591ac
commit 9aac209c72
11 changed files with 414 additions and 80 deletions

View File

@@ -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

View File

@@ -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"),

View File

@@ -0,0 +1,19 @@
<html>
<body>
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.
<p>Configure the inspection:</p>
<p>
Use the <b>Convert to escape sequences</b> quick-fix to ensure compatibility with ISO-8859-1 encoding:
</p>
<p><b>Example:</b></p>
<pre><code>
key=Java + ☕ = 🍀
</code></pre>
<p>After the quick-fix is applied:</p>
<pre><code>
key=Java + \u2615 = \uD83C\uDF40
</code></pre>
</body>
</html>

View File

@@ -1895,6 +1895,7 @@ f:com.intellij.openapi.fileTypes.BinaryFileTypeDecompilers
f:com.intellij.openapi.fileTypes.CharsetUtil
- <init>():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

View File

@@ -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<ByteBuffer> 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);
}
}

View File

@@ -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<ProblemDescriptor> descriptors) {
CharBuffer buffer = CharBuffer.wrap(text);
int textLength = text.length();
CharBuffer back = CharBuffer.allocate(textLength); // must be enough, error otherwise
Ref<ByteBuffer> 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<ByteBuffer> 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);

View File

@@ -58,6 +58,11 @@
groupBundle="messages.PropertiesBundle" groupKey="properties.files.inspection.group.display.name"
enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.lang.properties.UnusedMessageFormatParameterInspection"/>
<localInspection groupPath="Java" language="UAST" shortName="UnsupportedCharacter" bundle="messages.JavaI18nBundle"
key="unsupported.character.display.name"
groupBundle="messages.PropertiesBundle" groupKey="properties.files.inspection.group.display.name"
enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.lang.properties.UnsupportedCharacterInspection"/>
<localInspection groupPath="Java" language="JAVA" shortName="DuplicateStringLiteralInspection" bundle="messages.JavaI18nBundle"
key="inspection.duplicates.display.name" groupKey="group.names.internationalization.issues" groupBundle="messages.InspectionsBundle" enabledByDefault="false"
level="WARNING"

View File

@@ -88,6 +88,9 @@ rename.prefix.based.property.key.error.message=Could not rename property key ''{
resource.bundle.contains.locales.with.suspicious.locale.languages.desciptor=Resource bundle contains locales with suspicious locale languages
unused.message.format.parameter.display.name=Missing message format parameter
unused.message.format.parameter.problem.descriptor=Using parameter {0} without using parameter {1} in <code>#ref<code> #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

View File

@@ -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");
}
}
}

View File

@@ -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(<weak_warning descr="Unsupported characters for the charset 'ISO-8859-1'">"<caret>key1"</weak_warning>);
}
}
""");
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 = <weak_warning descr="Unsupported characters for the charset 'ISO-8859-1'">"<caret>key1"</weak_warning>;
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("<caret>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("<caret>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("<caret>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("<caret>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());
}
}

View File

@@ -16,7 +16,7 @@
<orderEntry type="module" module-name="intellij.xml.psi" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.lang" />
<orderEntry type="module" module-name="intellij.platform.boot" />
<orderEntry type="module" module-name="intellij.platform.boot" exported="" />
<orderEntry type="module" module-name="intellij.platform.util.jdom" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
</component>