From 31462ed80b4b1493eefe5008af323c95b2d6397a Mon Sep 17 00:00:00 2001 From: Max Medvedev Date: Thu, 13 Mar 2025 13:48:49 +0100 Subject: [PATCH] IDEA-359853 convert JShell parser to KMP GitOrigin-RevId: a11082e3c00bdcf766c5e2ad5a2c3f14287d774b --- .../syntax/JShellSyntaxDefinitionExtension.kt | 14 ++ .../resources/META-INF/JavaPsiPlugin.xml | 3 + .../lang/java/JShellFileElementType.java | 35 +++++ .../lang/java/JShellParserDefinition.java | 24 +--- .../lang/java/parser/JShellParser.java | 122 ------------------ .../java/syntax/JShellElementTypeConverter.kt | 20 +++ .../syntax/element/JShellSyntaxElementType.kt | 25 ++++ .../intellij/java/syntax/parser/FileParser.kt | 30 ++--- .../java/syntax/parser/JShellParser.kt | 112 ++++++++++++++++ .../java/parser/JavaParsingTestCase.java | 7 + .../java/parser/jshell/JShellParsingTest.java | 16 +++ 11 files changed, 248 insertions(+), 160 deletions(-) create mode 100644 java/java-frontback-psi-impl/src/com/intellij/java/frontback/psi/impl/syntax/JShellSyntaxDefinitionExtension.kt create mode 100644 java/java-psi-impl/src/com/intellij/lang/java/JShellFileElementType.java delete mode 100644 java/java-psi-impl/src/com/intellij/lang/java/parser/JShellParser.java create mode 100644 java/java-psi-impl/src/com/intellij/lang/java/syntax/JShellElementTypeConverter.kt create mode 100644 java/java-syntax/src/com/intellij/java/syntax/element/JShellSyntaxElementType.kt create mode 100644 java/java-syntax/src/com/intellij/java/syntax/parser/JShellParser.kt diff --git a/java/java-frontback-psi-impl/src/com/intellij/java/frontback/psi/impl/syntax/JShellSyntaxDefinitionExtension.kt b/java/java-frontback-psi-impl/src/com/intellij/java/frontback/psi/impl/syntax/JShellSyntaxDefinitionExtension.kt new file mode 100644 index 000000000000..fb29d1581ca6 --- /dev/null +++ b/java/java-frontback-psi-impl/src/com/intellij/java/frontback/psi/impl/syntax/JShellSyntaxDefinitionExtension.kt @@ -0,0 +1,14 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.frontback.psi.impl.syntax + +import com.intellij.java.syntax.JavaSyntaxDefinition +import com.intellij.platform.syntax.SyntaxElementTypeSet +import com.intellij.platform.syntax.lexer.Lexer +import com.intellij.platform.syntax.psi.LanguageSyntaxDefinition +import com.intellij.pom.java.LanguageLevel + +internal class JShellSyntaxDefinitionExtension : LanguageSyntaxDefinition { + override fun getLexer(): Lexer = JavaSyntaxDefinition.createLexer(LanguageLevel.HIGHEST) + + override fun getCommentTokens(): SyntaxElementTypeSet = JavaSyntaxDefinition.commentSet +} \ No newline at end of file diff --git a/java/java-psi-impl/resources/META-INF/JavaPsiPlugin.xml b/java/java-psi-impl/resources/META-INF/JavaPsiPlugin.xml index d7395449af53..c8ef17fb4466 100644 --- a/java/java-psi-impl/resources/META-INF/JavaPsiPlugin.xml +++ b/java/java-psi-impl/resources/META-INF/JavaPsiPlugin.xml @@ -106,6 +106,7 @@ + @@ -136,6 +137,8 @@ + + IMPORT_PARSED_CONDITION = tokenType -> JavaElementType.IMPORT_STATEMENT == tokenType; - private static final Predicate DECLARATION_PARSED_CONDITION = tokenType -> TOP_LEVEL_DECLARATIONS.contains(tokenType); - - private final FileParser myJShellFileParser = new FileParser(this) { - private final TokenSet IMPORT_PARSING_STOP_LIST = TokenSet.orSet( - IMPORT_LIST_STOPPER_SET, - TokenSet.orSet( - ElementType.MODIFIER_BIT_SET, - ElementType.JAVA_COMMENT_BIT_SET, - ElementType.EXPRESSION_BIT_SET, - ElementType.JAVA_STATEMENT_BIT_SET, - ElementType.PRIMITIVE_TYPE_BIT_SET, - TokenSet.create(JShellElementType.ROOT_CLASS, JavaTokenType.IDENTIFIER) - ) - ); - - @Override - public void parse(@NotNull PsiBuilder builder) { - parseImportList(builder, b -> IMPORT_PARSING_STOP_LIST.contains(b.getTokenType())); - - final PsiBuilder.Marker rootClass = builder.mark(); - try { - while (!builder.eof()) { - PsiBuilder.Marker wrapper = builder.mark(); - IElementType wrapperType = null; - - PsiBuilder.Marker marker = parseImportStatement(builder); - if (isParsed(marker, builder, IMPORT_PARSED_CONDITION)) { - wrapperType = JShellElementType.IMPORT_HOLDER; - } - else { - revert(marker); - marker = getDeclarationParser().parse(builder, DeclarationParser.Context.JSHELL); - if (isParsed(marker, builder, DECLARATION_PARSED_CONDITION) && !((PsiBuilderImpl)builder).hasErrorsAfter(marker)) { - wrapper.drop(); // don't need wrapper for top-level declaration - wrapper = null; - } - else { - revert(marker); - marker = getStatementParser().parseStatement(builder); - if (marker != null && !((PsiBuilderImpl)builder).hasErrorsAfter(marker)) { - wrapperType = JShellElementType.STATEMENTS_HOLDER; - } - else { - revert(marker); - marker = getExpressionParser().parse(builder); - wrapperType = marker != null ? JShellElementType.STATEMENTS_HOLDER : null; - } - } - } - - if (marker == null) { - wrapper.drop(); - break; - } - - if (wrapper != null) { - wrapper.done(wrapperType); - } - } - - if (!builder.eof()) { - builder.mark().error(JavaPsiBundle.message("unexpected.token")); - while (!builder.eof()) { - builder.advanceLexer(); - } - } - } - finally { - rootClass.done(JShellElementType.ROOT_CLASS); - } - } - }; - - private static boolean isParsed(@Nullable PsiBuilder.Marker parsedMarker, PsiBuilder builder, final Predicate cond) { - if (parsedMarker == null) { - return false; - } - final LighterASTNode lastDone = builder.getLatestDoneMarker(); - if (lastDone == null) { - return false; - } - return cond.test(lastDone.getTokenType()); - } - - private static void revert(PsiBuilder.Marker parsedMarker) { - if (parsedMarker != null) { - parsedMarker.rollbackTo(); - } - } - - @Override - public @NotNull FileParser getFileParser() { - return myJShellFileParser; - } -} diff --git a/java/java-psi-impl/src/com/intellij/lang/java/syntax/JShellElementTypeConverter.kt b/java/java-psi-impl/src/com/intellij/lang/java/syntax/JShellElementTypeConverter.kt new file mode 100644 index 000000000000..cb0be62b25c6 --- /dev/null +++ b/java/java-psi-impl/src/com/intellij/lang/java/syntax/JShellElementTypeConverter.kt @@ -0,0 +1,20 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.lang.java.syntax + +import com.intellij.java.syntax.element.JShellSyntaxElementType +import com.intellij.lang.java.JShellParserDefinition +import com.intellij.platform.syntax.psi.ElementTypeConverter +import com.intellij.platform.syntax.psi.ElementTypeConverterFactory +import com.intellij.platform.syntax.psi.elementTypeConverterOf +import com.intellij.psi.impl.source.tree.JShellElementType + +internal class JShellElementTypeConverterExtension : ElementTypeConverterFactory { + override fun getElementTypeConverter(): ElementTypeConverter = JShellElementTypeConverter +} + +internal val JShellElementTypeConverter: ElementTypeConverter = elementTypeConverterOf( + JShellSyntaxElementType.FILE to JShellParserDefinition.FILE_ELEMENT_TYPE, + JShellSyntaxElementType.ROOT_CLASS to JShellElementType.ROOT_CLASS, + JShellSyntaxElementType.IMPORT_HOLDER to JShellElementType.IMPORT_HOLDER, + JShellSyntaxElementType.STATEMENTS_HOLDER to JShellElementType.STATEMENTS_HOLDER, +) \ No newline at end of file diff --git a/java/java-syntax/src/com/intellij/java/syntax/element/JShellSyntaxElementType.kt b/java/java-syntax/src/com/intellij/java/syntax/element/JShellSyntaxElementType.kt new file mode 100644 index 000000000000..f82a20d285fc --- /dev/null +++ b/java/java-syntax/src/com/intellij/java/syntax/element/JShellSyntaxElementType.kt @@ -0,0 +1,25 @@ +/* + * 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.java.syntax.element + +import com.intellij.platform.syntax.SyntaxElementType + +object JShellSyntaxElementType { + val FILE: SyntaxElementType = SyntaxElementType("JSHELL_FILE") + val ROOT_CLASS: SyntaxElementType = SyntaxElementType("JSHELL_ROOT_CLASS") + val STATEMENTS_HOLDER: SyntaxElementType = SyntaxElementType("JSHELL_STATEMENTS_HOLDER") + val IMPORT_HOLDER: SyntaxElementType = SyntaxElementType("JSHELL_IMPORT_HOLDER") +} diff --git a/java/java-syntax/src/com/intellij/java/syntax/parser/FileParser.kt b/java/java-syntax/src/com/intellij/java/syntax/parser/FileParser.kt index ca1f492a17e9..f87ca8458f2a 100644 --- a/java/java-syntax/src/com/intellij/java/syntax/parser/FileParser.kt +++ b/java/java-syntax/src/com/intellij/java/syntax/parser/FileParser.kt @@ -1,13 +1,11 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.java.syntax.parser -import com.intellij.java.syntax.JavaSyntaxBundle.message -import com.intellij.java.syntax.JavaSyntaxBundle.resourceBundle +import com.intellij.java.syntax.JavaSyntaxBundle import com.intellij.java.syntax.element.JavaSyntaxElementType import com.intellij.java.syntax.element.JavaSyntaxTokenType import com.intellij.java.syntax.element.SyntaxElementTypes import com.intellij.java.syntax.element.WhiteSpaceAndCommentSetHolder -import com.intellij.openapi.util.NlsContexts import com.intellij.platform.syntax.SyntaxElementType import com.intellij.platform.syntax.SyntaxElementTypeSet import com.intellij.platform.syntax.i18n.ResourceBundle @@ -16,8 +14,12 @@ import com.intellij.platform.syntax.syntaxElementTypeSetOf import com.intellij.platform.syntax.util.parser.SyntaxBuilderUtil.expect open class FileParser(private val myParser: JavaParser) { - fun parse(builder: SyntaxTreeBuilder) { - parseFile(builder, { b -> this.stopImportListParsing(b) }, resourceBundle, "expected.class.or.interface") + open fun parse(builder: SyntaxTreeBuilder) { + parseFile(builder = builder, + importListStopper = { b -> this.stopImportListParsing(b) }, + bundle = JavaSyntaxBundle.resourceBundle, + errorMessageKey = "expected.class.or.interface" + ) } fun parseFile( @@ -44,7 +46,7 @@ open class FileParser(private val myParser: JavaParser) { if (declaration == null) declaration = parseInitial(builder) if (declaration != null) { if (invalidElements != null) { - invalidElements.errorBefore(error(bundle, errorMessageKey), declaration) + invalidElements.errorBefore(bundle.message(errorMessageKey), declaration) invalidElements = null } if (firstDeclarationOk == null) { @@ -66,7 +68,7 @@ open class FileParser(private val myParser: JavaParser) { if (firstDeclarationOk == null) firstDeclarationOk = false } - invalidElements?.error(error(bundle, errorMessageKey)) + invalidElements?.error(bundle.message(errorMessageKey)) if (impListInfo.second && firstDeclarationOk == true) { impListInfo.first.setCustomEdgeTokenBinders(myWhiteSpaceAndCommentSetHolder.getPrecedingCommentBinder(myParser.languageLevel), @@ -108,7 +110,7 @@ open class FileParser(private val myParser: JavaParser) { val ref = myParser.referenceParser.parseJavaCodeReference(builder, true, false, false, false) if (ref == null) { - statement.error(message("expected.class.or.interface")) + statement.error(JavaSyntaxBundle.message("expected.class.or.interface")) return } @@ -117,7 +119,7 @@ open class FileParser(private val myParser: JavaParser) { JavaParserUtil.done(statement, JavaSyntaxElementType.PACKAGE_STATEMENT, myParser.languageLevel, myWhiteSpaceAndCommentSetHolder) } - private fun parseImportList( + protected fun parseImportList( builder: SyntaxTreeBuilder, stopper: (SyntaxTreeBuilder) -> Boolean, ): Pair { @@ -138,7 +140,7 @@ open class FileParser(private val myParser: JavaParser) { if (statement != null) { isEmpty = false if (invalidElements != null) { - invalidElements.errorBefore(message("unexpected.token"), statement) + invalidElements.errorBefore(JavaSyntaxBundle.message("unexpected.token"), statement) invalidElements = null } continue @@ -180,10 +182,11 @@ open class FileParser(private val myParser: JavaParser) { } //if it is `module` we should expect either `;` or `identifier` - if (isOk && !isModule && !isStatic && builder.tokenType !== JavaSyntaxTokenType.SEMICOLON && + if (isOk && !isModule && + !isStatic && builder.tokenType !== JavaSyntaxTokenType.SEMICOLON && PsiKeyword.MODULE == identifierText ) { - JavaParserUtil.error(builder, message("expected.identifier.or.semicolon")) + JavaParserUtil.error(builder, JavaSyntaxBundle.message("expected.identifier.or.semicolon")) } else if (isOk) { JavaParserUtil.semicolon(builder) @@ -209,9 +212,6 @@ open class FileParser(private val myParser: JavaParser) { return JavaSyntaxElementType.IMPORT_STATEMENT } - private fun error(bundle: ResourceBundle, errorMessageKey: String): @NlsContexts.ParsingError String { - return bundle.message(errorMessageKey) - } } val IMPORT_LIST_STOPPER_SET: SyntaxElementTypeSet = diff --git a/java/java-syntax/src/com/intellij/java/syntax/parser/JShellParser.kt b/java/java-syntax/src/com/intellij/java/syntax/parser/JShellParser.kt new file mode 100644 index 000000000000..200c252c5185 --- /dev/null +++ b/java/java-syntax/src/com/intellij/java/syntax/parser/JShellParser.kt @@ -0,0 +1,112 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.syntax.parser + +import com.intellij.java.syntax.JavaSyntaxBundle +import com.intellij.java.syntax.element.JShellSyntaxElementType +import com.intellij.java.syntax.element.JavaSyntaxElementType +import com.intellij.java.syntax.element.JavaSyntaxTokenType +import com.intellij.java.syntax.element.SyntaxElementTypes +import com.intellij.platform.syntax.SyntaxElementType +import com.intellij.platform.syntax.SyntaxElementTypeSet +import com.intellij.platform.syntax.parser.SyntaxTreeBuilder +import com.intellij.platform.syntax.syntaxElementTypeSetOf +import com.intellij.pom.java.LanguageLevel +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +class JShellParser(languageLevel: LanguageLevel) : JavaParser(languageLevel) { + override val fileParser: FileParser = object : FileParser(this) { + override fun parse(builder: SyntaxTreeBuilder) { + parseImportList(builder) { b -> b.tokenType in IMPORT_PARSING_STOP_LIST } + + val rootClass: SyntaxTreeBuilder.Marker = builder.mark() + try { + while (!builder.eof()) { + var wrapper: SyntaxTreeBuilder.Marker? = builder.mark() + var wrapperType: SyntaxElementType? = null + + var marker: SyntaxTreeBuilder.Marker? = parseImportStatement(builder) + if (isParsed(marker, builder) { tokenType -> tokenType === JavaSyntaxElementType.IMPORT_STATEMENT }) { + wrapperType = JShellSyntaxElementType.IMPORT_HOLDER + } + else { + marker?.rollbackTo() + marker = declarationParser.parse(builder, DeclarationParser.Context.JSHELL) + if (isParsed(marker, builder) { tokenType -> tokenType in TOP_LEVEL_DECLARATIONS } && + !builder.hasErrorsAfter(marker) + ) { + wrapper!!.drop() // don't need wrapper for top-level declaration + wrapper = null + } + else { + marker?.rollbackTo() + marker = statementParser.parseStatement(builder) + if (marker != null && !builder.hasErrorsAfter(marker)) { + wrapperType = JShellSyntaxElementType.STATEMENTS_HOLDER + } + else { + marker?.rollbackTo() + marker = expressionParser.parse(builder) + wrapperType = if (marker != null) JShellSyntaxElementType.STATEMENTS_HOLDER else null + } + } + } + + if (marker == null) { + wrapper!!.drop() + break + } + + wrapper?.done(wrapperType!!) + } + + if (!builder.eof()) { + builder.mark().error(JavaSyntaxBundle.message("unexpected.token")) + while (!builder.eof()) { + builder.advanceLexer() + } + } + } + finally { + rootClass.done(JShellSyntaxElementType.ROOT_CLASS) + } + } + } + + fun parse(builder: SyntaxTreeBuilder) { + val root = builder.mark() + fileParser.parse(builder) + root.done(JShellSyntaxElementType.FILE) + } + + @OptIn(ExperimentalContracts::class) + private fun isParsed( + parsedMarker: SyntaxTreeBuilder.Marker?, + builder: SyntaxTreeBuilder, + cond: (SyntaxElementType) -> Boolean, + ): Boolean { + contract { + returns(true) implies (parsedMarker != null) + } + if (parsedMarker == null) return false + val lastDone = builder.lastDoneMarker ?: return false + return cond(lastDone.getNodeType()) + } +} + +private val TOP_LEVEL_DECLARATIONS: SyntaxElementTypeSet = syntaxElementTypeSetOf( + JavaSyntaxElementType.FIELD, + JavaSyntaxElementType.METHOD, + JavaSyntaxElementType.CLASS +) + +private val IMPORT_PARSING_STOP_LIST: SyntaxElementTypeSet = + IMPORT_LIST_STOPPER_SET + + SyntaxElementTypes.MODIFIER_BIT_SET + + SyntaxElementTypes.JAVA_COMMENT_BIT_SET + + SyntaxElementTypes.EXPRESSION_BIT_SET + + SyntaxElementTypes.JAVA_STATEMENT_BIT_SET + + SyntaxElementTypes.PRIMITIVE_TYPE_BIT_SET + + JShellSyntaxElementType.ROOT_CLASS + + JavaSyntaxTokenType.IDENTIFIER + diff --git a/java/java-tests/testSrc/com/intellij/java/parser/JavaParsingTestCase.java b/java/java-tests/testSrc/com/intellij/java/parser/JavaParsingTestCase.java index a380fa0fa540..724e6718df4c 100644 --- a/java/java-tests/testSrc/com/intellij/java/parser/JavaParsingTestCase.java +++ b/java/java-tests/testSrc/com/intellij/java/parser/JavaParsingTestCase.java @@ -2,11 +2,16 @@ package com.intellij.java.parser; import com.intellij.JavaTestUtil; +import com.intellij.java.frontback.psi.impl.syntax.JavaSyntaxDefinitionExtension; import com.intellij.lang.LanguageASTFactory; import com.intellij.lang.ParserDefinition; +import com.intellij.lang.java.JShellLanguage; import com.intellij.lang.java.JavaLanguage; +import com.intellij.lang.java.syntax.JavaElementTypeConverterExtension; import com.intellij.openapi.roots.LanguageLevelProjectExtension; import com.intellij.openapi.roots.impl.LanguageLevelProjectExtensionImpl; +import com.intellij.platform.syntax.psi.ElementTypeConverters; +import com.intellij.platform.syntax.psi.LanguageSyntaxDefinitions; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.PsiFile; import com.intellij.psi.impl.source.tree.JavaASTFactory; @@ -29,6 +34,8 @@ public abstract class JavaParsingTestCase extends ParsingTestCase { getProject().registerService(WorkspaceModelTopics.class, new WorkspaceModelTopics()); getProject().registerService(LanguageLevelProjectExtension.class, new LanguageLevelProjectExtensionImpl(getProject())); addExplicitExtension(LanguageASTFactory.INSTANCE, JavaLanguage.INSTANCE, new JavaASTFactory()); + addExplicitExtension(LanguageSyntaxDefinitions.getINSTANCE(), JavaLanguage.INSTANCE, new JavaSyntaxDefinitionExtension()); + addExplicitExtension(ElementTypeConverters.getInstance(), JavaLanguage.INSTANCE, new JavaElementTypeConverterExtension()); } @Override diff --git a/java/java-tests/testSrc/com/intellij/java/parser/jshell/JShellParsingTest.java b/java/java-tests/testSrc/com/intellij/java/parser/jshell/JShellParsingTest.java index 8eefa5520d5a..84ccedc75bb1 100644 --- a/java/java-tests/testSrc/com/intellij/java/parser/jshell/JShellParsingTest.java +++ b/java/java-tests/testSrc/com/intellij/java/parser/jshell/JShellParsingTest.java @@ -2,9 +2,17 @@ package com.intellij.java.parser.jshell; import com.intellij.ide.highlighter.JShellFileType; +import com.intellij.java.frontback.psi.impl.syntax.JShellSyntaxDefinitionExtension; +import com.intellij.java.frontback.psi.impl.syntax.JavaSyntaxDefinitionExtension; import com.intellij.java.parser.JavaParsingTestCase; +import com.intellij.lang.java.JShellLanguage; import com.intellij.lang.java.JShellParserDefinition; +import com.intellij.lang.java.JavaLanguage; import com.intellij.lang.java.JavaParserDefinition; +import com.intellij.lang.java.syntax.JShellElementTypeConverterExtension; +import com.intellij.lang.java.syntax.JavaElementTypeConverterExtension; +import com.intellij.platform.syntax.psi.ElementTypeConverters; +import com.intellij.platform.syntax.psi.LanguageSyntaxDefinitions; public class JShellParsingTest extends JavaParsingTestCase { @@ -12,6 +20,14 @@ public class JShellParsingTest extends JavaParsingTestCase { super("parser-full/jshell", JShellFileType.DEFAULT_EXTENSION, new JShellParserDefinition(), new JavaParserDefinition()); } + @Override + protected void setUp() throws Exception { + super.setUp(); + addExplicitExtension(ElementTypeConverters.getInstance(), JShellLanguage.INSTANCE, new JavaElementTypeConverterExtension()); + addExplicitExtension(ElementTypeConverters.getInstance(), JShellLanguage.INSTANCE, new JShellElementTypeConverterExtension()); + addExplicitExtension(LanguageSyntaxDefinitions.getINSTANCE(), JShellLanguage.INSTANCE, new JShellSyntaxDefinitionExtension()); + } + public void testClass0() { doTest(); } public void testStatement0() { doTest(); } public void testStatement1() { doTest(); }