diff --git a/java/java-frontback-tests/BUILD.bazel b/java/java-frontback-tests/BUILD.bazel index a5e3db1f1062..88d1550922f8 100644 --- a/java/java-frontback-tests/BUILD.bazel +++ b/java/java-frontback-tests/BUILD.bazel @@ -37,6 +37,7 @@ jvm_library( "//java/testFramework/shared", "//platform/syntax/syntax-psi:psi", "//java/java-syntax:syntax", + "//platform/syntax/syntax-tree:tree", ], exports = [ "@lib//:easymock", diff --git a/java/java-frontback-tests/intellij.java.frontback.tests.iml b/java/java-frontback-tests/intellij.java.frontback.tests.iml index ad5a6265bd8a..259415074fa1 100644 --- a/java/java-frontback-tests/intellij.java.frontback.tests.iml +++ b/java/java-frontback-tests/intellij.java.frontback.tests.iml @@ -28,5 +28,6 @@ + \ No newline at end of file diff --git a/java/java-frontback-tests/testSrc/com/intellij/java/parser/AbstractBasicJavaParsingTestCase.java b/java/java-frontback-tests/testSrc/com/intellij/java/parser/AbstractBasicJavaParsingTestCase.java index d1c192f3b3fc..d12aa5805f93 100644 --- a/java/java-frontback-tests/testSrc/com/intellij/java/parser/AbstractBasicJavaParsingTestCase.java +++ b/java/java-frontback-tests/testSrc/com/intellij/java/parser/AbstractBasicJavaParsingTestCase.java @@ -8,8 +8,11 @@ import com.intellij.lang.LanguageExtension; import com.intellij.lang.java.JavaLanguage; import com.intellij.lang.java.JavaParserDefinition; import com.intellij.lang.java.parser.BasicJavaParserUtil; +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.util.io.FileUtil; import com.intellij.platform.backend.workspace.WorkspaceModelTopics; import com.intellij.platform.syntax.psi.LanguageSyntaxDefinitions; +import com.intellij.platform.syntax.tree.SyntaxNode; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.PsiFile; import com.intellij.psi.impl.DebugUtil; @@ -17,11 +20,14 @@ import com.intellij.testFramework.ParsingTestCase; import com.intellij.testFramework.TestDataFile; import org.jetbrains.annotations.NotNull; +import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; public abstract class AbstractBasicJavaParsingTestCase extends ParsingTestCase { private final AbstractBasicJavaParsingTestConfigurator myConfigurator; + private SyntaxNode myKmpTree; @SuppressWarnings("JUnitTestCaseWithNonTrivialConstructors") public AbstractBasicJavaParsingTestCase(String dataPath, AbstractBasicJavaParsingTestConfigurator configurator) { @@ -50,6 +56,14 @@ public abstract class AbstractBasicJavaParsingTestCase extends ParsingTestCase { addExplicitExtension(collector, language, object); } + public final void addExplicit(@NotNull ExtensionPointName extensionPointName, @NotNull T extension) { + registerExtension(extensionPointName, extension); + } + + public final void addExtensionPoint(@NotNull ExtensionPointName extensionPointName, @NotNull Class extensionClass) { + super.registerExtensionPoint(extensionPointName, extensionClass); + } + @Override protected PsiFile createFile(@NotNull String name, @NotNull String text) { PsiFile file = super.createFile(name, text); @@ -81,13 +95,42 @@ public abstract class AbstractBasicJavaParsingTestCase extends ParsingTestCase { } String treeDump = DebugUtil.nodeTreeAsElementTypeToString(file.getNode(), !skipSpaces()).trim(); - doCheckResult(myFullDataPath, targetDataName + "_node.txt", treeDump); + assertSameLinesWithFile(getExpectedFileName(targetDataName), treeDump); + + checkKmpTree(targetDataName); + } + + private void checkKmpTree(@NotNull String targetDataName) { + String kmpTreeDump = JavaTestSyntaxParsingUtil.dumpTree(myKmpTree); + + String expectedFileName = getExpectedFileName(targetDataName); + String expectedText; + try { + expectedText = FileUtil.loadFile(new File(expectedFileName), StandardCharsets.UTF_8); + } + catch (IOException e) { + throw new RuntimeException(e); + } + + String normalizedExpected = JavaTestSyntaxParsingUtil.removeEmptyListLines(expectedText); + assertSameLines(normalizedExpected, kmpTreeDump); + } + + private @NotNull String getExpectedFileName(@NotNull String targetDataName) { + return myFullDataPath + File.separatorChar + targetDataName + "_node.txt"; + } + + @Override + protected @NotNull PsiFile parseFile(@NotNull String name, @NotNull String text) { + myKmpTree = myConfigurator.createFileSyntaxNode(text, null); + return super.parseFile(name, text); } protected void doParserTest(@NotNull String text, BasicJavaParserUtil.@NotNull ParserWrapper parser) { String name = getTestName(false); myFile = myConfigurator.createPsiFile(this, name, text, parser); + myKmpTree = myConfigurator.createFileSyntaxNode(text, parser); try { checkResult(name, myFile); } diff --git a/java/java-frontback-tests/testSrc/com/intellij/java/parser/AbstractBasicJavaParsingTestConfigurator.java b/java/java-frontback-tests/testSrc/com/intellij/java/parser/AbstractBasicJavaParsingTestConfigurator.java index 5206eac7b687..5e41d9b75ac6 100644 --- a/java/java-frontback-tests/testSrc/com/intellij/java/parser/AbstractBasicJavaParsingTestConfigurator.java +++ b/java/java-frontback-tests/testSrc/com/intellij/java/parser/AbstractBasicJavaParsingTestConfigurator.java @@ -2,9 +2,11 @@ package com.intellij.java.parser; import com.intellij.lang.java.parser.BasicJavaParserUtil; +import com.intellij.platform.syntax.tree.SyntaxNode; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.PsiFile; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public interface AbstractBasicJavaParsingTestConfigurator { void setUp(@NotNull AbstractBasicJavaParsingTestCase thinJavaParsingTestCase); @@ -17,6 +19,14 @@ public interface AbstractBasicJavaParsingTestConfigurator { @NotNull String text, @NotNull BasicJavaParserUtil.ParserWrapper parser); + /** + * @param text text to parse + * @param parserWrapper parser to use or `null` if we want to parse the whole file + * @return SyntaxNode node representing the parsed content + */ + @NotNull + SyntaxNode createFileSyntaxNode(@NotNull String text, @Nullable BasicJavaParserUtil.ParserWrapper parserWrapper); + boolean checkPsi(); void setLanguageLevel(@NotNull LanguageLevel level); diff --git a/java/java-frontback-tests/testSrc/com/intellij/java/parser/JavaParsingTestConfiguratorBase.kt b/java/java-frontback-tests/testSrc/com/intellij/java/parser/JavaParsingTestConfiguratorBase.kt index d30a28e98d60..e7e35cb44133 100644 --- a/java/java-frontback-tests/testSrc/com/intellij/java/parser/JavaParsingTestConfiguratorBase.kt +++ b/java/java-frontback-tests/testSrc/com/intellij/java/parser/JavaParsingTestConfiguratorBase.kt @@ -2,15 +2,19 @@ package com.intellij.java.parser import com.intellij.ide.highlighter.JavaFileType +import com.intellij.java.syntax.JavaSyntaxDefinition +import com.intellij.java.syntax.element.JavaLanguageLevelProvider import com.intellij.lang.ASTNode import com.intellij.lang.LanguageASTFactory import com.intellij.lang.java.JavaLanguage import com.intellij.lang.java.parser.BasicJavaParserUtil import com.intellij.lang.java.parser.JavaParserUtil import com.intellij.lang.java.syntax.JavaElementTypeConverterExtension +import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.platform.syntax.SyntaxElementType import com.intellij.platform.syntax.parser.SyntaxTreeBuilder import com.intellij.platform.syntax.psi.* +import com.intellij.platform.syntax.tree.SyntaxNode import com.intellij.pom.java.LanguageLevel import com.intellij.psi.* import com.intellij.psi.impl.source.PsiJavaFileImpl @@ -36,8 +40,12 @@ abstract class JavaParsingTestConfiguratorBase( testCase.addExplicit(languageASTFactory, java, JavaASTFactory()) testCase.clearCachesOfLanguageExtension(java, languageASTFactory) } - } + ExtensionPointName("com.intellij.java.syntax.languageLevelProvider").let { ep -> + testCase.addExtensionPoint(ep, JavaLanguageLevelProvider::class.java) + testCase.addExplicit(ep, JavaLanguageLevelProvider { this.languageLevel }) + } + } override fun setLanguageLevel(level: LanguageLevel) { this.level = level @@ -52,6 +60,18 @@ abstract class JavaParsingTestConfiguratorBase( ourLanguageLevel = languageLevel } + override fun createFileSyntaxNode(text: String, parserWrapper: BasicJavaParserUtil.ParserWrapper?): SyntaxNode { + ourLanguageLevel = languageLevel + return parseForSyntaxTree(text) { builder -> + if (parserWrapper != null) { + parseWithWrapper(builder, parserWrapper) + } + else { + JavaSyntaxDefinition.parse(ourLanguageLevel, builder) + } + } + } + override fun createPsiFile( thinJavaParsingTestCase: AbstractBasicJavaParsingTestCase, name: String, diff --git a/java/java-frontback-tests/testSrc/com/intellij/java/parser/JavaTestSyntaxParsingUtil.kt b/java/java-frontback-tests/testSrc/com/intellij/java/parser/JavaTestSyntaxParsingUtil.kt new file mode 100644 index 000000000000..8de814df8814 --- /dev/null +++ b/java/java-frontback-tests/testSrc/com/intellij/java/parser/JavaTestSyntaxParsingUtil.kt @@ -0,0 +1,67 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + +@file:JvmName("JavaTestSyntaxParsingUtil") + +package com.intellij.java.parser + +import com.intellij.java.syntax.JavaSyntaxDefinition +import com.intellij.platform.syntax.parser.SyntaxTreeBuilder +import com.intellij.platform.syntax.tree.KmpSyntaxNode + +import com.intellij.platform.syntax.tree.SyntaxNode +import com.intellij.platform.syntax.tree.children +import com.intellij.platform.syntax.tree.parse +import com.intellij.platform.syntax.util.language.SyntaxElementLanguageProvider + +internal fun parseForSyntaxTree( + text: String, + parser: ((SyntaxTreeBuilder) -> Unit), +): KmpSyntaxNode { + val mapper = SyntaxElementLanguageProvider { + sequenceOf(JavaSyntaxDefinition.language) + } + + val tree = parse( + text = text, + lexerFactory = JavaSyntaxDefinition::createLexer, + parser = parser, + whitespaces = JavaSyntaxDefinition.whitespaces, + comments = JavaSyntaxDefinition.comments, + languageMapper = mapper, + cancellationProvider = null, + logger = null, + whitespaceOrCommentBindingPolicy = JavaSyntaxDefinition.whitespaceOrCommentBindingPolicy, + ) + + return tree +} + +internal fun dumpTree(node: SyntaxNode): String { + val all = mutableListOf() + + fun append(node: SyntaxNode) { + all.add(node) + for (child in node.children()) { + append(child) + } + } + append(node) + + val parsing = all.joinToString("\n") { node -> + val type = node.type + val prefix = " ".repeat((node.level()) * 2) + "$prefix$type" + } + return parsing +} + +private fun SyntaxNode.level(): Int = generateSequence(this) { it.parent() }.count() - 1 + +/** + * remove `` lines from the expected tree + * kmp tree cannot have them because "composite/non-composite" node information is missing in the KMP tree + */ +internal fun String.removeEmptyListLines(): String = + lines() + .filterNot { it.trim() == "" } + .joinToString("\n")