From e742beae770e5a22e195d08cc38d08a3d694dff8 Mon Sep 17 00:00:00 2001 From: Stephan Herrmann Date: Fri, 16 Aug 2024 23:49:29 +0200 Subject: [PATCH 1/4] [23] Code completion for module imports (#2825) * completing module names after "import module" - more relevance to modules that are read by the current module * completing modifiers "module" / "static" after "import " fixes https://github.com/eclipse-jdt/eclipse.jdt.core/issues/2823 --- .../tests/model/AbstractJavaModelTests.java | 2 +- .../core/tests/model/CompletionTests23.java | 322 ++++++++++++++++++ .../tests/model/RunCompletionModelTests.java | 1 + .../workspace/Completion23/.classpath | 10 + .../workspace/Completion23/.project | 17 + .../workspace/Completion23/mod.one.jar | Bin 0 -> 1199 bytes .../workspace/Completion23/mod.two.jar | Bin 0 -> 1264 bytes .../workspace/Completion23/src/.keep | 1 + .../internal/codeassist/CompletionEngine.java | 42 ++- .../codeassist/complete/CompletionParser.java | 4 +- .../codeassist/impl/AssistParser.java | 9 +- 11 files changed, 397 insertions(+), 11 deletions(-) create mode 100644 org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/CompletionTests23.java create mode 100644 org.eclipse.jdt.core.tests.model/workspace/Completion23/.classpath create mode 100644 org.eclipse.jdt.core.tests.model/workspace/Completion23/.project create mode 100644 org.eclipse.jdt.core.tests.model/workspace/Completion23/mod.one.jar create mode 100644 org.eclipse.jdt.core.tests.model/workspace/Completion23/mod.two.jar create mode 100644 org.eclipse.jdt.core.tests.model/workspace/Completion23/src/.keep diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AbstractJavaModelTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AbstractJavaModelTests.java index 060cdca74d9..7d5ae68f645 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AbstractJavaModelTests.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AbstractJavaModelTests.java @@ -3612,7 +3612,7 @@ protected void setUpProjectCompliance(IJavaProject javaProject, String complianc new Path(newJclSrcString), entry.getSourceAttachmentRootPath(), entry.getAccessRules(), - new IClasspathAttribute[0], + entry.getExtraAttributes(), entry.isExported()); jclPathEntrySet = true; } diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/CompletionTests23.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/CompletionTests23.java new file mode 100644 index 00000000000..6fe34bb71aa --- /dev/null +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/CompletionTests23.java @@ -0,0 +1,322 @@ +/******************************************************************************* + * Copyright (c) 2024 GK Software SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * This is an implementation of an early-draft specification developed under the Java + * Community Process (JCP) and is made available for testing and evaluation purposes + * only. The code is not compatible with any specification of the JCP. + * + * Contributors: + * Stephan Herrmann - initial API and implementation + *******************************************************************************/ + +package org.eclipse.jdt.core.tests.model; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; + +import junit.framework.Test; + +public class CompletionTests23 extends AbstractJavaModelCompletionTests { + +static { +// TESTS_NAMES = new String[] {"test006"}; +} + +private static int DEFAULT_RELEVANCE = R_DEFAULT + R_RESOLVED + R_INTERESTING + R_CASE + R_NON_RESTRICTED; + +private IJavaProject completion23Project; + +public CompletionTests23(String name) { + super(name); +} + +@Override +public void setUpSuite() throws Exception { + this.completion23Project = setUpJavaProject("Completion23", "23", false); + this.completion23Project.setOption(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.ENABLED); + super.setUpSuite(); +} +@Override +public void tearDownSuite() throws Exception { + deleteProject(this.completion23Project); + super.tearDownSuite(); +} +public static Test suite() { + return buildModelTestSuite(CompletionTests23.class); +} + +public void testKeyword() throws JavaModelException { + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy( + "/Completion23/src/p/X.java", + """ + package p; + import m + public class X {} + """); + + CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true); + requestor.allowAllRequiredProposals(); + String str = this.workingCopies[0].getSource(); + String completeBehind = "import m"; + int cursorLocation = str.lastIndexOf(completeBehind) + completeBehind.length(); + this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner); + int relevanceWithoutCase = DEFAULT_RELEVANCE - R_CASE; + assertResults( + "Map[TYPE_REF]{java.util.Map;, java.util, Ljava.util.Map;, null, null, "+relevanceWithoutCase+"}\n" + + "MethodHandle[TYPE_REF]{java.lang.invoke.MethodHandle;, java.lang.invoke, Ljava.lang.invoke.MethodHandle;, null, null, "+relevanceWithoutCase+"}\n" + + "MethodHandles[TYPE_REF]{java.lang.invoke.MethodHandles;, java.lang.invoke, Ljava.lang.invoke.MethodHandles;, null, null, "+relevanceWithoutCase+"}\n" + + "MethodType[TYPE_REF]{java.lang.invoke.MethodType;, java.lang.invoke, Ljava.lang.invoke.MethodType;, null, null, "+relevanceWithoutCase+"}\n" + + "module[KEYWORD]{module, null, null, module, null, " + DEFAULT_RELEVANCE + "}", + requestor.getResults()); +} +public void testKeyword_neg() throws JavaModelException { + // keyword not enabled, other proposals don't match + this.completion23Project.setOption(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.DISABLED); + try { + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy("/Completion23/src/p/X.java", """ + package p; + import mod + public class X {} + """); + CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true); + requestor.allowAllRequiredProposals(); + String str = this.workingCopies[0].getSource(); + String completeBehind = "import mod"; + int cursorLocation = str.lastIndexOf(completeBehind) + completeBehind.length(); + this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner); + assertResults( + "", + requestor.getResults()); + } finally { + this.completion23Project.setOption(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.ENABLED); + } +} + +public void test001() throws JavaModelException { + // prefixed + // only java.base available (from JCL_23_LIB) + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy( + "/Completion23/src/p/X.java", + """ + package p; + import module java. + public class X {} + """); + + CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true); + requestor.allowAllRequiredProposals(); + String str = this.workingCopies[0].getSource(); + String completeBehind = "java."; + int cursorLocation = str.lastIndexOf(completeBehind) + completeBehind.length(); + this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner); + assertResults( + "[MODULE_REF]{java.base, java.base, null, null, null, " + DEFAULT_RELEVANCE + "}", + requestor.getResults()); +} + +public void test001_neg() throws JavaModelException { + // prefixed + // only java.base available (from JCL_23_LIB) + // preview JEP 476 not enabled + // other proposals don't match prefix + this.completion23Project.setOption(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.DISABLED); + try { + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy( + "/Completion23/src/p/X.java", + """ + package p; + import module java.b + public class X {} + """); + + CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true); + requestor.allowAllRequiredProposals(); + String str = this.workingCopies[0].getSource(); + String completeBehind = "java.b"; + int cursorLocation = str.lastIndexOf(completeBehind) + completeBehind.length(); + this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner); + assertResults( + "", + requestor.getResults()); + } finally { + this.completion23Project.setOption(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.ENABLED); + } +} + +public void test002() throws JavaModelException { + // no prefix + // only java.base available (from JCL_23_LIB) + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy( + "/Completion23/src/p/X.java", + """ + package p; + import module\s + public class X {} + """); + + CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true); + requestor.allowAllRequiredProposals(); + String str = this.workingCopies[0].getSource(); + String completeBehind = "module "; + int cursorLocation = str.lastIndexOf(completeBehind) + completeBehind.length(); + this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner); + assertResults( + "[MODULE_REF]{java.base, java.base, null, null, null, " + DEFAULT_RELEVANCE + "}", + requestor.getResults()); +} + +public void test003() throws JavaModelException { + // no prefix + // 2 modules available: mod.one & java.base + // unnamed module reads them all + IPath jarPath = this.completion23Project.getPath().append("mod.one.jar"); + try { + addClasspathEntry(this.completion23Project, newModularLibraryEntry(jarPath, null, null)); + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy("/Completion23/src/p/X.java", """ + package p; + import module\s + public class X {} + """); + CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true); + requestor.allowAllRequiredProposals(); + String str = this.workingCopies[0].getSource(); + String completeBehind = "module "; + int cursorLocation = str.lastIndexOf(completeBehind) + completeBehind.length(); + this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner); + assertResults( + "[MODULE_REF]{java.base, java.base, null, null, null, " + DEFAULT_RELEVANCE + "}\n" + + "[MODULE_REF]{mod.one, mod.one, null, null, null, " + DEFAULT_RELEVANCE + "}", + requestor.getResults()); + } finally { + removeClasspathEntry(this.completion23Project, jarPath); + } +} + +public void test004() throws JavaModelException { + // with prefix + // 3 modules on the module path: mod.two, mod.one & java.base + // prefix selects 2 out of 3 + IPath jarOnePath = this.completion23Project.getPath().append("mod.one.jar"); + IPath jarTwoPath = this.completion23Project.getPath().append("mod.two.jar"); + try { + addClasspathEntry(this.completion23Project, newModularLibraryEntry(jarOnePath, null, null)); + addClasspathEntry(this.completion23Project, newModularLibraryEntry(jarTwoPath, null, null)); + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy("/Completion23/src/p/X.java", """ + package p; + import module mo + public class X {} + """); + CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true); + requestor.allowAllRequiredProposals(); + String str = this.workingCopies[0].getSource(); + String completeBehind = "module mo"; + int cursorLocation = str.lastIndexOf(completeBehind) + completeBehind.length(); + this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner); + assertResults( + "[MODULE_REF]{mod.one, mod.one, null, null, null, " + DEFAULT_RELEVANCE + "}\n" + + "[MODULE_REF]{mod.two, mod.two, null, null, null, " + DEFAULT_RELEVANCE + "}", + requestor.getResults()); + } finally { + removeClasspathEntry(this.completion23Project, jarOnePath); + removeClasspathEntry(this.completion23Project, jarTwoPath); + } +} + +public void test005() throws CoreException { + // with prefix + // 4 modules available: mod.test (self), mod.two (required), mod.one (transitively required) & java.base (from JCL_23_LIB) + // prefix selects 3 out of 4 + IPath jarOnePath = this.completion23Project.getPath().append("mod.one.jar"); + IPath jarTwoPath = this.completion23Project.getPath().append("mod.two.jar"); + IFile moduleFile = null; + try { + addClasspathEntry(this.completion23Project, newModularLibraryEntry(jarOnePath, null, null)); + addClasspathEntry(this.completion23Project, newModularLibraryEntry(jarTwoPath, null, null)); + moduleFile = createFile("Completion23/src/module-info.java", + """ + module mod.test { + requires mod.two; // mod.two requires transitive mod.one + } + """); + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy("/Completion23/src/p/X.java", """ + package p; + import module mo + public class X {} + """); + CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true); + requestor.allowAllRequiredProposals(); + String str = this.workingCopies[0].getSource(); + String completeBehind = "module mo"; + int cursorLocation = str.lastIndexOf(completeBehind) + completeBehind.length(); + this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner); + assertResults( + "[MODULE_REF]{mod.one, mod.one, null, null, null, " + DEFAULT_RELEVANCE + "}\n" + + "[MODULE_REF]{mod.test, mod.test, null, null, null, " + DEFAULT_RELEVANCE + "}\n" + + "[MODULE_REF]{mod.two, mod.two, null, null, null, " + DEFAULT_RELEVANCE + "}", + requestor.getResults()); + } finally { + removeClasspathEntry(this.completion23Project, jarOnePath); + removeClasspathEntry(this.completion23Project, jarTwoPath); + if (moduleFile != null) + deleteResource(moduleFile); + } +} +public void test006() throws CoreException { + // with prefix + // 4 modules present: mod.test(self), mod.one, mod.two & java.base available + // + prefix rules out java.base + // + mod.two is proposed with lower relevance, because it is not read by the current module + IPath jarOnePath = this.completion23Project.getPath().append("mod.one.jar"); + IPath jarTwoPath = this.completion23Project.getPath().append("mod.two.jar"); + try { + addClasspathEntry(this.completion23Project, newModularLibraryEntry(jarOnePath, null, null)); + addClasspathEntry(this.completion23Project, newModularLibraryEntry(jarTwoPath, null, null)); // not read my the current module + createFile("Completion23/src/module-info.java", + """ + module mod.test { + requires mod.one; + } + """); + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy("/Completion23/src/p/X.java", """ + package p; + import module mo + public class X {} + """); + CompletionTestsRequestor2 requestor = new CompletionTestsRequestor2(true); + requestor.allowAllRequiredProposals(); + String str = this.workingCopies[0].getSource(); + String completeBehind = "module mo"; + int cursorLocation = str.lastIndexOf(completeBehind) + completeBehind.length(); + this.workingCopies[0].codeComplete(cursorLocation, requestor, this.wcOwner); + assertResults( + "[MODULE_REF]{mod.two, mod.two, null, null, null, " + (DEFAULT_RELEVANCE - R_NON_RESTRICTED) + "}\n" + // lower relevance, not read + "[MODULE_REF]{mod.one, mod.one, null, null, null, " + DEFAULT_RELEVANCE + "}\n" + + "[MODULE_REF]{mod.test, mod.test, null, null, null, " + DEFAULT_RELEVANCE + "}", + requestor.getResults()); + } finally { + removeClasspathEntry(this.completion23Project, jarOnePath); + removeClasspathEntry(this.completion23Project, jarTwoPath); + } +} +} diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/RunCompletionModelTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/RunCompletionModelTests.java index 524de058fe2..073f1824eb1 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/RunCompletionModelTests.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/RunCompletionModelTests.java @@ -46,6 +46,7 @@ public class RunCompletionModelTests extends junit.framework.TestCase { COMPLETION_SUITES.add(CompletionTests16_1.class); COMPLETION_SUITES.add(CompletionTests16_2.class); COMPLETION_SUITES.add(CompletionTests17.class); + COMPLETION_SUITES.add(CompletionTests23.class); COMPLETION_SUITES.add(CompletionTestsForRecordPattern.class); COMPLETION_SUITES.add(CompletionContextTests.class); COMPLETION_SUITES.add(CompletionContextTests_1_5.class); diff --git a/org.eclipse.jdt.core.tests.model/workspace/Completion23/.classpath b/org.eclipse.jdt.core.tests.model/workspace/Completion23/.classpath new file mode 100644 index 00000000000..892c539c82a --- /dev/null +++ b/org.eclipse.jdt.core.tests.model/workspace/Completion23/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/org.eclipse.jdt.core.tests.model/workspace/Completion23/.project b/org.eclipse.jdt.core.tests.model/workspace/Completion23/.project new file mode 100644 index 00000000000..a5b73adad75 --- /dev/null +++ b/org.eclipse.jdt.core.tests.model/workspace/Completion23/.project @@ -0,0 +1,17 @@ + + + Completion23 + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/org.eclipse.jdt.core.tests.model/workspace/Completion23/mod.one.jar b/org.eclipse.jdt.core.tests.model/workspace/Completion23/mod.one.jar new file mode 100644 index 0000000000000000000000000000000000000000..1f3340d59f5487d2627d57e4672aecbcd74dc344 GIT binary patch literal 1199 zcmWIWW@Zs#;Nak3Xv^h~WIzHU3@i-3t|5-Po_=onzK(vLZmz*0dcJP|PBAci_C0gj z$6HtLBCofu*10q1HwPJ9F@Es$(NiXd0B?4VP4!Igq=80B0&xJ`0!}mwxPY>H$vKI| z#RZ8a8KwQF{SF%lxL!XS9(K^t=YB+_jX-B!7C?lr%uxFK+-O(K`1 z;~$lyky}KbU)7t|>nMNvoPS~97fW7C*}K;+y_vUWWmi5ZV3^o^UOohRyNMBBz;J*9 zrl2T4D>b>Kbb>G6AqA0^>)%9tzc;;$3D|X^g7GT@hd!_GPtFU+9&@uwXFtkIoAKSQ zp+w*Gnz4m`Z{BQWD-+XJ*eh7@>&k$fNe7q>Z zIQrLvunL)kb@PR;rgxjqdm!(`d5Tp@*N3mj^!5KYHS;cb>%WaUe%(B;?hP-mK~MwCG95_A_%r+}IdZH21DKvdP7b%`ASy1OsMkWoGMuD1CeW6Ax2T91ir=2_3s6KDM|o<8p#y5iX?R7!2Fa?yIkkb<=J0U9afSd{Oju$NT|07c gfPxVL#F$aFg90_co0Scuf)xl2fsV=ms$pON01#nw>;M1& literal 0 HcmV?d00001 diff --git a/org.eclipse.jdt.core.tests.model/workspace/Completion23/mod.two.jar b/org.eclipse.jdt.core.tests.model/workspace/Completion23/mod.two.jar new file mode 100644 index 0000000000000000000000000000000000000000..0e31c8085622d621a085dd639ff20048f7206b8c GIT binary patch literal 1264 zcmWIWW@Zs#;Nak3P{`wtWIzHU3@i-3t|5-Po_=onzK(vLZmz*0dcJP|PBAci_C0gj z$6HtLBCofu*10q1HwPJ9F@Es$(NiXd0B?4VP4!Igq=80B0&xJ`0xmQQxPY>H$vKI| z#RZ8a8AtoQ{SO-mw46V?zN2RS*~fwswP8&y+_*C+Zr{#DDkxOzW4 zlbgQP__y!Iiub;Di$329I(hD=QPJY=v4*-y>{jY8Hm2+iy{K^7-N5kV z10|!6FGbw1Imv|o-n^!~D85YUsDAsUZ)??9G%r?9zxR96-iD8yk5!yi9P&8-uk7tj z>`3IFykhwd=JKhv;7~J|>wEnlFig~$kVEZrE^4%JfI_XHC_gJTxukT0FW(^rk(TS< zMB4T)uomAEo3W>%ilJqC+p<3_8DD=m9XvK`$85=Gd-)f<_2#|JIcK`t^%o7_Gq`6- zE0%^Y{{427^`dPnSClSempk`h=VY@ZMZvi%(zv8jS@`^&PrgYy)f=?H+4-dV(G5Bu zJSXRWmUybLPOQS@=-fvVmz7vs6Jy#N`7c!+*vm0PTAX$EjXUhy4?JclzB^-uUG%0; zQ~n5e1?UN6CYDTC9r-`~`RA5-)tT3g_U$SBnw$?xCDtosLU#dulgEhcU-VQW2vm`q zpHiBWs+*aYmJdlV)?f3l+p)2+rL)PEv8lPSy}R>g%B49WchBtEQsc(9?!li+i=KpR zKQkx9jg3)7bMKlXn_S%3%;GmpFkrS;X0{H9TEE05enzA4sgviKPZby&6*P&wR#g>S zZf0O$V03^%nAvznN@4=*B_p1kfIvPrR)w=y4>W*W$<84zux)ZV&`nE$7?HLa(9*U5 znya%C%MwqY_18S>8G7QZ|3z)h^WiI=J<{>g)j10!!Zb9!&S;<0IjPg^@%;JHsN$=T ze01VeaE%_&Bm=Nrj7%a7h^&g7$3R&X72wLa0p18xKp~Bs=0N!o0TO{sMD9e^2TGC1 zaR*9~2rwNOo3QkWtP5lta@c|56#?3TX&xh%k+p*Y7&$mV0gM2)%&6Ky!5!eu$_7%w P3WSD0N38^^VPF6NGl-dJ literal 0 HcmV?d00001 diff --git a/org.eclipse.jdt.core.tests.model/workspace/Completion23/src/.keep b/org.eclipse.jdt.core.tests.model/workspace/Completion23/src/.keep new file mode 100644 index 00000000000..fd0dfa71768 --- /dev/null +++ b/org.eclipse.jdt.core.tests.model/workspace/Completion23/src/.keep @@ -0,0 +1 @@ +# force existence of the src folder \ No newline at end of file diff --git a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/CompletionEngine.java b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/CompletionEngine.java index 5e36c452ee2..6a8c3513217 100644 --- a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/CompletionEngine.java +++ b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/CompletionEngine.java @@ -8,6 +8,10 @@ * * SPDX-License-Identifier: EPL-2.0 * + * This is an implementation of an early-draft specification developed under the Java + * Community Process (JCP) and is made available for testing and evaluation purposes + * only. The code is not compatible with any specification of the JCP. + * * Contributors: * Timo Kinnunen - Contributions for bug 377373 - [subwords] known limitations with JDT 3.8 * Bug 420953 - [subwords] Constructors that don't match prefix not found @@ -746,6 +750,7 @@ private static boolean hasMemberTypesInEnclosingScope(SourceTypeBinding typeBind private INameEnvironment noCacheNameEnvironment; char[] source; ModuleDeclaration moduleDeclaration; + ModuleBinding currentModule; boolean skipDefaultPackage = false; char[] completionToken; @@ -1354,6 +1359,20 @@ public void acceptModule(char[] moduleName) { if (this.knownModules.containsKey(moduleName)) return; if (this.assistNodeInJavadoc == 0 && this.moduleDeclaration != null && CharOperation.equals(moduleName, this.moduleDeclaration.moduleName)) return; if (CharOperation.equals(moduleName, CharOperation.NO_CHAR)) return; + boolean isModuleRead; + if (this.currentModule == null || this.currentModule.isUnnamed()) { + isModuleRead = true; + } else if (CharOperation.equals(moduleName, this.currentModule.moduleName)) { + isModuleRead = true; + } else { + isModuleRead = false; // in a modular project reduce relevance for modules that are not (yet) read + for (ModuleBinding requiredModule : this.currentModule.getAllRequiredModules()) { + if (CharOperation.equals(moduleName, requiredModule.moduleName)) { + isModuleRead = true; + break; + } + } + } this.knownModules.put(moduleName, this); char[] completion = moduleName; int relevance = computeBaseRelevance(); @@ -1361,7 +1380,9 @@ public void acceptModule(char[] moduleName) { relevance += computeRelevanceForInterestingProposal(); relevance += computeRelevanceForCaseMatching(this.qualifiedCompletionToken == null ? this.completionToken : this.qualifiedCompletionToken, moduleName); relevance += computeRelevanceForQualification(true); - relevance += computeRelevanceForRestrictions(IAccessRule.K_ACCESSIBLE); + if (isModuleRead) { + relevance += computeRelevanceForRestrictions(IAccessRule.K_ACCESSIBLE); + } this.noProposal = false; if(!this.requestor.isIgnored(CompletionProposal.MODULE_REF)) { InternalCompletionProposal proposal = createProposal(CompletionProposal.MODULE_REF, this.actualCompletionPosition); @@ -2267,8 +2288,21 @@ public void complete(ICompilationUnit sourceUnit, int completionPosition, int po long positions = importReference.sourcePositions[importReference.tokens.length - 1]; setSourceAndTokenRange((int) (positions >>> 32), (int) positions); + if ((importReference.modifiers & ClassFileConstants.AccModule) != 0 && this.compilerOptions.enablePreviewFeatures) { + this.currentModule = this.unitScope.module(); // enable module-graph analysis for readability + this.completionToken = CharOperation.concatWithAll(importReference.tokens, '.'); + findModules(this.completionToken, true); + return; + } char[][] oldTokens = importReference.tokens; int tokenCount = oldTokens.length; + if (tokenCount <= 1 && this.compilerOptions.enablePreviewFeatures) { + char[][] choices = this.compilerOptions.enablePreviewFeatures + ? new char[][] { Keywords.STATIC, Keywords.MODULE } + : new char[][] { Keywords.STATIC }; + char[] token = tokenCount == 1 ? oldTokens[0] : CharOperation.NO_CHAR; + findKeywords(token, choices, false, false); + } if (tokenCount == 1) { findImports((CompletionOnImportReference)importReference, true); } else if(tokenCount > 1){ @@ -2964,7 +2998,7 @@ private void completionOnJavadocSingleTypeReference(ASTNode astNode, Scope scope (this.assistNodeInJavadoc & CompletionOnJavadoc.BASE_TYPES) != 0, false, new ObjectVector()); - findModules(typeRef, false); + findModules(typeRef.token, false); } //TODO private void completionOnJavadocModuleReference(ASTNode astNode, Binding qualifiedBinding, Scope scope) { @@ -11468,7 +11502,7 @@ private void findModules(CompletionOnJavadocQualifiedTypeReference typeReference } } } - private void findModules(CompletionOnJavadocSingleTypeReference typeReference, boolean targetted) { + private void findModules(char[] token, boolean targetted) { if (JavaCore.compareJavaVersions(this.sourceLevel, JavaCore.VERSION_15) >= 0 ) { boolean isIgnoredModuleRef = false; try { @@ -11476,7 +11510,7 @@ private void findModules(CompletionOnJavadocSingleTypeReference typeReference, b this.requestor.setIgnored(CompletionProposal.MODULE_REF, false); isIgnoredModuleRef = true; } - this.nameEnvironment.findModules(CharOperation.toLowerCase(typeReference.token), this, targetted ? this.javaProject : null); + this.nameEnvironment.findModules(CharOperation.toLowerCase(token), this, targetted ? this.javaProject : null); } finally { this.requestor.setIgnored(CompletionProposal.MODULE_REF, isIgnoredModuleRef); } diff --git a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/complete/CompletionParser.java b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/complete/CompletionParser.java index e19ec42663c..3c6b9f5a530 100644 --- a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/complete/CompletionParser.java +++ b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/complete/CompletionParser.java @@ -3987,8 +3987,8 @@ protected void consumeSingleMemberAnnotation(boolean isTypeAnnotation) { } } @Override -protected void consumeSingleStaticImportDeclarationName() { - super.consumeSingleStaticImportDeclarationName(); +protected void consumeSingleModifierImportDeclarationName(int modifier) { + super.consumeSingleModifierImportDeclarationName(modifier); this.pendingAnnotation = null; // the pending annotation cannot be attached to next nodes } @Override diff --git a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/impl/AssistParser.java b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/impl/AssistParser.java index 281f6dc345e..6084b8807b3 100644 --- a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/impl/AssistParser.java +++ b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/impl/AssistParser.java @@ -1075,8 +1075,9 @@ protected void consumeRestoreDiet() { } } @Override -protected void consumeSingleStaticImportDeclarationName() { - // SingleTypeImportDeclarationName ::= 'import' 'static' Name +protected void consumeSingleModifierImportDeclarationName(int modifier) { + // SingleStaticImportDeclarationName ::= 'import' 'static' Name RejectTypeAnnotations + // SingleModuleImportDeclarationName ::= 'import' 'module' Name RejectTypeAnnotations /* push an ImportRef build from the last name stored in the identifier stack. */ @@ -1084,7 +1085,7 @@ protected void consumeSingleStaticImportDeclarationName() { /* no need to take action if not inside assist identifiers */ if ((index = indexOfAssistIdentifier()) < 0) { - super.consumeSingleStaticImportDeclarationName(); + super.consumeSingleModifierImportDeclarationName(modifier); return; } /* retrieve identifiers subset and whole positions, the assist node positions @@ -1102,7 +1103,7 @@ protected void consumeSingleStaticImportDeclarationName() { length); /* build specific assist node on import statement */ - ImportReference reference = createAssistImportReference(subset, positions, ClassFileConstants.AccStatic); + ImportReference reference = createAssistImportReference(subset, positions, modifier); this.assistNode = reference; this.lastCheckPoint = reference.sourceEnd + 1; From 9b33f168119500e61bc71845b62a3cde77721d5e Mon Sep 17 00:00:00 2001 From: Stephan Herrmann Date: Sun, 18 Aug 2024 23:25:20 +0200 Subject: [PATCH 2/4] [23] Parsing details for markdown comments #2824 (#2831) + codeblock cannot interrupt a paragraph + separate handling of code blocks indented vs fenced + fence does not terminate indented code block + handle various forms of fences: - ` vs. ~ - different fence lengths - no mixed fences - fences inside fenced region + handle (and require) escaping of [ ] inside references (method arguments) fixes https://github.com/eclipse-jdt/eclipse.jdt.core/issues/2824 --- .../parser/AbstractCommentParser.java | 37 ++- .../parser/IMarkdownCommentHelper.java | 61 +++- .../regression/MarkdownCommentsTest.java | 31 ++- .../tests/dom/ASTConverterMarkdownTest.java | 263 ++++++++++++++++++ 4 files changed, 371 insertions(+), 21 deletions(-) diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/AbstractCommentParser.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/AbstractCommentParser.java index c114c7d946a..90a2904715c 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/AbstractCommentParser.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/AbstractCommentParser.java @@ -246,6 +246,9 @@ protected boolean commentParse() { consumeToken(); } + if (this.markdown && !Character.isWhitespace(nextCharacter) && nextCharacter != '/' && nextCharacter != '`') { + this.markdownHelper.recordText(); + } // Consume rules depending on the read character switch (nextCharacter) { case '@' : @@ -469,8 +472,8 @@ protected boolean commentParse() { this.textStart = this.index; break; } - } else if (nextCharacter == '`') { - this.markdownHelper.recordBackTick(this.lineStarted); + } else if (nextCharacter == '`' || nextCharacter == '~') { + this.markdownHelper.recordFenceChar(previousChar, nextCharacter, this.lineStarted); } } if (isFormatterParser && nextCharacter == '<') { @@ -650,12 +653,12 @@ protected Object parseArguments(Object receiver, boolean checkVerifySpaceOrEndCo // Read possible additional type info dim = 0; isVarargs = false; - if (readToken() == TerminalTokens.TokenNameLBRACKET) { + if (readMarkdownEscapedToken(TerminalTokens.TokenNameLBRACKET)) { // array declaration - while (readToken() == TerminalTokens.TokenNameLBRACKET) { + while (readMarkdownEscapedToken(TerminalTokens.TokenNameLBRACKET)) { int dimStart = this.scanner.getCurrentTokenStartPosition(); consumeToken(); - if (readToken() != TerminalTokens.TokenNameRBRACKET) { + if (!readMarkdownEscapedToken(TerminalTokens.TokenNameRBRACKET)) { break nextArg; } consumeToken(); @@ -731,6 +734,10 @@ protected Object parseArguments(Object receiver, boolean checkVerifySpaceOrEndCo } // Something wrong happened => Invalid input + if (this.markdown) { + // skip over bogus token + this.currentTokenType = -1; + } throw Scanner.invalidInput(); } finally { // we have to make sure that this is reset to the previous value even if an exception occurs @@ -3333,6 +3340,26 @@ protected int readToken() throws InvalidInputException { return this.currentTokenType; } + protected boolean readMarkdownEscapedToken(int expectedToken) throws InvalidInputException { + if (!this.markdown) + return readToken() == expectedToken; + if (this.currentTokenType < 0) { + this.tokenPreviousPosition = this.scanner.currentPosition; + if (peekChar() != '\\') + return false; + this.scanner.currentPosition++; + this.currentTokenType = this.scanner.getNextToken(); + if (this.currentTokenType != expectedToken) { + this.scanner.currentPosition = this.tokenPreviousPosition; + this.currentTokenType = -1; + return false; + } + this.index = this.scanner.currentPosition; + this.lineStarted = true; // after having read a token, line is obviously started... + } + return this.currentTokenType == expectedToken; + } + protected int readTokenAndConsume() throws InvalidInputException { int token = readToken(); consumeToken(); diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/IMarkdownCommentHelper.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/IMarkdownCommentHelper.java index fd41f6ce070..b8689f5205c 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/IMarkdownCommentHelper.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/IMarkdownCommentHelper.java @@ -26,7 +26,7 @@ public interface IMarkdownCommentHelper { void recordSlash(int nextIndex); - void recordBackTick(boolean lineStarted); + void recordFenceChar(char previous, char next, boolean lineStarted); /** * When at the beginning of a comment line, record that a whitespace was seen. @@ -35,6 +35,8 @@ public interface IMarkdownCommentHelper { */ boolean recordSignificantLeadingSpace(); + void recordText(); + boolean isInCodeBlock(); /** Retrieve the start of the current text, possibly including significant leading whitespace. */ @@ -56,7 +58,6 @@ static IMarkdownCommentHelper create(AbstractCommentParser parser) { return new NullMarkdownHelper(); } } - } class NullMarkdownHelper implements IMarkdownCommentHelper { @@ -67,7 +68,7 @@ public void recordSlash(int nextIndex) { // nop } @Override - public void recordBackTick(boolean lineStarted) { + public void recordFenceChar(char previous, char next, boolean lineStarted) { // nop } @Override @@ -75,6 +76,10 @@ public boolean recordSignificantLeadingSpace() { return false; } @Override + public void recordText() { + // nop + } + @Override public boolean isInCodeBlock() { return false; } @@ -97,8 +102,13 @@ class MarkdownCommentHelper implements IMarkdownCommentHelper { int slashCount = 0; int leadingSpaces = 0; int markdownLineStart = -1; - int backTickCount = 0; - boolean insideFence = false; + boolean insideIndentedCodeBlock = false; + boolean insideFencedCodeBlock = false; + char fenceChar; + int fenceCharCount; + int fenceLength; + boolean isBlankLine = true; + boolean previousIsBlankLine = true; public MarkdownCommentHelper(int lineStart, int commonIndent) { this.markdownLineStart = lineStart; @@ -116,29 +126,48 @@ public void recordSlash(int nextIndex) { } @Override - public void recordBackTick(boolean lineStarted) { - if (this.backTickCount < 3) { - if (this.backTickCount == 0 && lineStarted) { + public void recordFenceChar(char previous, char next, boolean lineStarted) { + if (this.insideIndentedCodeBlock) { + return; + } + if (this.fenceCharCount == 0) { + if (lineStarted) return; - } - if (++this.backTickCount == 3) { - this.insideFence^=true; - } + this.fenceChar = next; + this.fenceCharCount = 1; + return; + } + if (next != this.fenceChar || previous != next) + return; + int required = this.insideFencedCodeBlock ? this.fenceLength : 3; + if (++this.fenceCharCount == required) { + this.insideFencedCodeBlock^=true; } + this.fenceLength = this.fenceCharCount; } @Override public boolean recordSignificantLeadingSpace() { if (this.markdownLineStart != -1) { - if (++this.leadingSpaces > this.commonIndent) + if (++this.leadingSpaces > this.commonIndent) { + if (!this.insideFencedCodeBlock && this.previousIsBlankLine && this.leadingSpaces - this.commonIndent >= 4) + this.insideIndentedCodeBlock = true; return true; + } } return false; } + @Override + public void recordText() { + this.isBlankLine = false; + if (this.leadingSpaces - this.commonIndent < 4) + this.insideIndentedCodeBlock = false; + } + @Override public boolean isInCodeBlock() { - return this.insideFence || (this.leadingSpaces - this.commonIndent >= 4); + return this.insideIndentedCodeBlock || this.insideFencedCodeBlock; } @Override @@ -156,10 +185,12 @@ public void resetLineStart() { @Override public void resetAtLineEnd() { + this.previousIsBlankLine = this.isBlankLine; + this.isBlankLine = true; this.slashCount = 0; this.leadingSpaces = 0; this.markdownLineStart = -1; - this.backTickCount = 0; + this.fenceCharCount = 0; // do not reset `insideFence` } } \ No newline at end of file diff --git a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/MarkdownCommentsTest.java b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/MarkdownCommentsTest.java index 6ffc762c163..6e72f9b8468 100644 --- a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/MarkdownCommentsTest.java +++ b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/MarkdownCommentsTest.java @@ -301,6 +301,7 @@ public void test010() { public class X { /// Some text here without the necessary tags for main method /// @param arguments array of strings + /// @return java.lang.Str -- should not raise an error /// no tags here public static void main(String[] arguments) { @@ -309,7 +310,7 @@ public static void main(String[] arguments) { } """, }, "----------\n" + - "1. ERROR in X.java (at line 6)\n" + + "1. ERROR in X.java (at line 7)\n" + " public static void main(String[] arguments) {\n" + " ^^^^^^^^^\n" + "Javadoc: Missing tag for parameter arguments\n" + @@ -596,4 +597,32 @@ public static void main(String[] args) { this.reportMissingJavadocTags = bkup; } } + public void test021() { + // arrays in method reference lack escaping. + // TODO specific error message? + String bkup = this.reportMissingJavadocTags; + try { + this.reportMissingJavadocTags = CompilerOptions.IGNORE; + this.runNegativeTest(new String[] { "X.java", + """ + /// + /// Reference to method with array parameter: [#main(String[])] + /// + public class X { + public static void main(String[] args) { + System.out.println("Hello"); + } + } + """, }, + "----------\n" + + "1. ERROR in X.java (at line 2)\n" + + " /// Reference to method with array parameter: [#main(String[])]\n" + + " ^^^^^^^^\n" + + "Javadoc: Invalid parameters declaration\n" + + "----------\n", + JavacTestOptions.Excuse.EclipseWarningConfiguredAsError); + } finally { + this.reportMissingJavadocTags = bkup; + } + } } diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTConverterMarkdownTest.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTConverterMarkdownTest.java index 11603641161..4325ea23868 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTConverterMarkdownTest.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTConverterMarkdownTest.java @@ -1337,6 +1337,27 @@ private void verifyJavadoc(Javadoc docComment) { } */ + protected void assertTagsAndTexts(List tagList, String[] tags, String[][] liness) { + assertEquals(this.prefix+"Wrong number of tags", tags.length, tagList.size()); + + for (int i = 0; i < liness.length; i++) { + ASTNode tagNode = tagList.get(i); + String tag = tags[i]; + assertEquals(this.prefix+"Invalid type for fragment ["+tagNode+"]", ASTNode.TAG_ELEMENT, tagNode.getNodeType()); + TagElement tagElement = (TagElement) tagNode; + assertEquals(this.prefix+"Invalid tag", tag, tagElement.getTagName()); + List fragments = tagElement.fragments(); + String[] lines = liness[i]; + assertEquals(this.prefix+"Wrong number of fragments", lines.length, fragments.size()); + for (int j = 0; j < lines.length; j++) { + ASTNode fragment = fragments.get(j); + String line = lines[j]; + assertEquals(this.prefix+"Invalid type for fragment ["+fragment+"]", ASTNode.TEXT_ELEMENT, fragment.getNodeType()); + assertEquals(this.prefix+"Wrong text content", line, ((TextElement) fragment).getText()); + } + } + } + /** * Check javadoc for MethodDeclaration */ @@ -3368,4 +3389,246 @@ void numberOfSpaces2() { } } } } + + public void testGH2808_codeAfterPara() throws JavaModelException { + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy("/Converter_23/src/markdown/gh2808/CodeAfterPara.java", + """ + package markdown.gh2808; + + public class CodeAfterPara { + /// Plain Text + /// @Override public void four() // four significant spaces but no blank line + void noBlankLine() { } + + /// Plain Text + /// \s + /// @Override public void four() // four significant spaces after blank line + void withBlankLine() { } + } + """ + ); + CompilationUnit compilUnit = (CompilationUnit) runConversion(this.workingCopies[0], true); + if (this.docCommentSupport.equals(JavaCore.ENABLED)) { + List unitComments = compilUnit.getCommentList(); + assertEquals("Wrong number of comments", 2, unitComments.size()); + + { // comments on noBlankLine() + Comment comment = (Comment) unitComments.get(0); + assertEquals("Comment should be javadoc", comment.getNodeType(), ASTNode.JAVADOC); + List tagList = ((Javadoc) comment).tags(); + + String[] tags = { + null, + "@Override" // parsed as tag, due to lack of blank line + }; + String[][] lines = { + {"Plain Text"}, + {" public void four() // four significant spaces but no blank line"} + }; + assertTagsAndTexts(tagList, tags, lines); + } + + { // comments on withBlankLine() + Comment comment = (Comment) unitComments.get(1); + assertEquals("Comment should be javadoc", comment.getNodeType(), ASTNode.JAVADOC); + String[] tags = { + null + }; + String[][] lines = { + { // one TagElement with 2 TextElements + "Plain Text", + " @Override public void four() // four significant spaces after blank line" + } + }; + assertTagsAndTexts(((Javadoc) comment).tags(), tags, lines); + + } + } + } + + public void testGH2808_terminatingAnIndentedCodeBlock() throws JavaModelException { + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy("/Converter_23/src/markdown/gh2808/BlockEnding.java", + """ + package markdown.gh2808; + + public class BlockEnding { + /// Plain Text + /// + /// @Override public void four() + /// ``` + /// /// doc + /// /// ``` + /// /// @Override Nested Code + /// /// ``` + /// ``` + void indentedWithFence() { } + + /// Plain Text + /// + /// @Override public void four() + /// Plain again + void paraAfterCode() { } + } + """ + ); + CompilationUnit compilUnit = (CompilationUnit) runConversion(this.workingCopies[0], true); + if (this.docCommentSupport.equals(JavaCore.ENABLED)) { + List unitComments = compilUnit.getCommentList(); + assertEquals("Wrong number of comments", 2, unitComments.size()); + + { // comments on indentedWithFence(): fence does not terminate indented code block, even nested doc comment is give verbatim + Comment comment = (Comment) unitComments.get(0); + assertEquals("Comment should be javadoc", comment.getNodeType(), ASTNode.JAVADOC); + List tagList = ((Javadoc) comment).tags(); + + String[] tags = { + null + }; + String[][] lines = { + { // one TagElement with many TextElements + "Plain Text", + " @Override public void four()", + " ```", + " /// doc", + " /// ```", + " /// @Override Nested Code", + " /// ```", + " ```" + } + }; + assertTagsAndTexts(tagList, tags, lines); + } + + { // comments on paraAfterCode() (requires jdt.ui to see what is rendered as code) + Comment comment = (Comment) unitComments.get(1); + assertEquals("Comment should be javadoc", comment.getNodeType(), ASTNode.JAVADOC); + String[] tags = { + null + }; + String[][] lines = { + { + "Plain Text", + " @Override public void four()", + "Plain again" + } + }; + assertTagsAndTexts(((Javadoc) comment).tags(), tags, lines); + } + } + } + + public void testGH2808_fencedCode() throws JavaModelException { + // fence can only be terminated by same number of same fence chars + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy("/Converter_23/src/markdown/gh2808/FencedCode.java", + """ + package markdown.gh2808; + + public class FencedCode { + /// ``~~mix is not a fence + /// Plain Text + /// + /// ~~~~script + /// @Override // not a tag even after ... + /// ~~~ + /// or + /// ```` + /// @Override + /// ~~~~ + void indentedWithFence() { } + } + """ + ); + CompilationUnit compilUnit = (CompilationUnit) runConversion(this.workingCopies[0], true); + if (this.docCommentSupport.equals(JavaCore.ENABLED)) { + List unitComments = compilUnit.getCommentList(); + assertEquals("Wrong number of comments", 1, unitComments.size()); + + Comment comment = (Comment) unitComments.get(0); + assertEquals("Comment should be javadoc", comment.getNodeType(), ASTNode.JAVADOC); + List tagList = ((Javadoc) comment).tags(); + + String[] tags = { + null + }; + String[][] lines = { + { // one TagElement with many TextElements + "``~~mix is not a fence", + "Plain Text", + "~~~~script", + "@Override // not a tag even after ...", + "~~~", + "or", + "````", + "@Override", + "~~~~" + } + }; + assertTagsAndTexts(tagList, tags, lines); + } + } + + public void testGH2808_linkWithArrayReference() throws JavaModelException { + // for a negative variant see org.eclipse.jdt.core.tests.compiler.regression.MarkdownCommentsTest.test021() + this.workingCopies = new ICompilationUnit[1]; + this.workingCopies[0] = getWorkingCopy("/Converter_23/src/markdown/gh2808/LinkWithArray.java", + """ + package markdown.gh2808; + + /// + /// Simple escaped link [#m1(int\\[\\])]. + /// Escaped link with custom text [method 1][#m1(int\\[\\])]. + /// + public class LinkWithArray { + public void m1(int[] i) {} + } + """ + ); + CompilationUnit compilUnit = (CompilationUnit) runConversion(this.workingCopies[0], true); + if (this.docCommentSupport.equals(JavaCore.ENABLED)) { + List unitComments = compilUnit.getCommentList(); + assertEquals("Wrong number of comments", 1, unitComments.size()); + + Comment comment = (Comment) unitComments.get(0); + assertEquals("Comment should be javadoc", comment.getNodeType(), ASTNode.JAVADOC); + List tagList = ((Javadoc) comment).tags(); + assertEquals("Should be one tag element", 1, tagList.size()); + TagElement tagElement = (TagElement) tagList.get(0); + tagList = tagElement.fragments(); + + String[] tags = { + null, + "@link", + null, + null, + "@link", + null + }; + String[] lines = { + "Simple escaped link ", + "m1(int [])", + ".", + "Escaped link with custom text ", + "method 1", + "." + }; + for (int i = 0; i < lines.length; i++) { + String tag = tags[i]; + String line = lines[i]; + ASTNode elem = (ASTNode) tagList.get(i); + if (tag != null) { + assertEquals("Node type", ASTNode.TAG_ELEMENT, elem.getNodeType()); + assertEquals("Tag name", tag, ((TagElement) elem).getTagName()); + elem = (ASTNode) ((TagElement) elem).fragments().get(0); + if (!(elem instanceof TextElement)) + continue; // @link without custom text + } else { + assertEquals("Node type", ASTNode.TEXT_ELEMENT, elem.getNodeType()); + } + assertEquals("Text", line, ((TextElement) elem).getText()); + } + } + } } From 4ed16449b0f437f64fdcfaaacd7e99a4f4949e71 Mon Sep 17 00:00:00 2001 From: Stephan Herrmann Date: Tue, 20 Aug 2024 20:23:57 +0200 Subject: [PATCH 3/4] [23] DOM support for JEP 476: Module Import Declarations (Preview) (#2836) + parser to record modifier start position of imports + avoid code duplication in SourceElementParser + new list ImportDeclaration.modifiers() + at 23 even 'static' is represented in that list + existing accessors may scan that list if present + adaptations in other parts of DOM implementation + consistently use Modifier even for static at JLS23 + enable DOM testing at JLS23 fixes https://github.com/eclipse-jdt/eclipse.jdt.core/issues/2834 --- .../compiler/ast/ImportReference.java | 1 + .../jdt/internal/compiler/parser/Parser.java | 9 +- .../core/tests/dom/ASTConverter_23Test.java | 112 ++++++++++++++ .../eclipse/jdt/core/tests/dom/ASTTest.java | 29 +++- .../jdt/core/tests/dom/RunConverterTests.java | 1 + .../workspace/Converter_23/.classpath | 6 +- org.eclipse.jdt.core/.settings/.api_filters | 14 ++ .../dom/org/eclipse/jdt/core/dom/AST.java | 3 + .../eclipse/jdt/core/dom/ASTConverter.java | 29 ++-- .../org/eclipse/jdt/core/dom/ASTMatcher.java | 6 +- .../jdt/core/dom/ImportDeclaration.java | 138 +++++++++++++++++- .../org/eclipse/jdt/core/dom/Modifier.java | 26 +++- .../internal/core/dom/NaiveASTFlattener.java | 13 +- .../core/dom/rewrite/ASTRewriteFlattener.java | 10 +- .../compiler/SourceElementParser.java | 77 +--------- 15 files changed, 381 insertions(+), 93 deletions(-) create mode 100644 org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTConverter_23Test.java diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/ImportReference.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/ImportReference.java index 7514a3b55cc..28685a3538b 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/ImportReference.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/ImportReference.java @@ -33,6 +33,7 @@ public class ImportReference extends ASTNode { public int declarationSourceStart; public int declarationSourceEnd; public int modifiers; // 1.5 addition for static imports + public int modifiersSourceStart; public Annotation[] annotations; // star end position public int trailingStarPosition; diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/Parser.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/Parser.java index 0995e493357..25b507ada82 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/Parser.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/Parser.java @@ -8894,7 +8894,13 @@ protected void consumeSingleModifierImportDeclarationName(int modifier) { pushOnAstStack(impt = new ImportReference(tokens, positions, false, modifier)); this.modifiers = ClassFileConstants.AccDefault; - this.modifiersSourceStart = -1; // <-- see comment into modifiersFlag(int) + // 'module' stores position on stack, 'static' sets modifiersSourceStart: + if (modifier == ClassFileConstants.AccModule) { + impt.modifiersSourceStart = this.intStack[this.intPtr--]; + } else { // static + impt.modifiersSourceStart = this.modifiersSourceStart; + } + this.modifiersSourceStart = -1; // see checkAndSetModifiers() if (this.currentToken == TokenNameSEMICOLON){ impt.declarationSourceEnd = this.scanner.currentPosition - 1; @@ -9334,6 +9340,7 @@ protected void consumeStaticImportOnDemandDeclarationName() { // star end position impt.trailingStarPosition = this.intStack[this.intPtr--]; this.modifiers = ClassFileConstants.AccDefault; + impt.modifiersSourceStart = this.modifiersSourceStart; this.modifiersSourceStart = -1; // <-- see comment into modifiersFlag(int) if (this.currentToken == TokenNameSEMICOLON){ diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTConverter_23Test.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTConverter_23Test.java new file mode 100644 index 00000000000..f04b6b03bd7 --- /dev/null +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTConverter_23Test.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2024 GK Software SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * This is an implementation of an early-draft specification developed under the Java + * Community Process (JCP) and is made available for testing and evaluation purposes + * only. The code is not compatible with any specification of the JCP. + * + * Contributors: + * Stephan Herrmann - Initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.core.tests.dom; + +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.Modifier; + +import junit.framework.Test; + +public class ASTConverter_23Test extends ConverterTestSetup { + + ICompilationUnit workingCopy; + + public void setUpSuite() throws Exception { + super.setUpSuite(); + this.ast = AST.newAST(getAST23(), false); + this.currentProject = getJavaProject("Converter_23"); + this.currentProject.setOption(JavaCore.COMPILER_COMPLIANCE, JavaCore.VERSION_23); + this.currentProject.setOption(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_23); + this.currentProject.setOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, JavaCore.VERSION_23); + this.currentProject.setOption(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.ENABLED); + this.currentProject.setOption(JavaCore.COMPILER_PB_REPORT_PREVIEW_FEATURES, JavaCore.IGNORE); + } + + public ASTConverter_23Test(String name) { + super(name); + } + + public static Test suite() { + return buildModelTestSuite(ASTConverter_23Test.class); + } + + static int getAST23() { + return AST.JLS23; + } + protected void tearDown() throws Exception { + super.tearDown(); + if (this.workingCopy != null) { + this.workingCopy.discardWorkingCopy(); + this.workingCopy = null; + } + } + + + public void test001() throws CoreException { + String contents = """ + package p; + import module java.base; + import static java.lang.System.out; + class X { + void m() { + out.println(Map.class.toString()); + } + } + """; + this.workingCopy = getWorkingCopy("/Converter_23/src/p/X.java", true/*resolve*/); + ASTNode node = buildAST( + contents, + this.workingCopy); + assertEquals("Not a compilation unit", ASTNode.COMPILATION_UNIT, node.getNodeType()); + CompilationUnit compilationUnit = (CompilationUnit) node; + assertProblemsSize(compilationUnit, 0); + List imports = compilationUnit.imports(); + assertEquals("Incorrect no of imports", 2, imports.size()); + + { + ImportDeclaration imp = imports.get(0); + assertEquals("Incorrect modifier bits", Modifier.MODULE, imp.getModifiers()); + assertEquals("Incorrect no of modifiers", 1, imp.modifiers().size()); + Modifier mod = (Modifier) imp.modifiers().get(0); + assertEquals("Incorrect modifier", "module", mod.getKeyword().toString()); + assertEquals("Incorrect modifier", Modifier.ModifierKeyword.MODULE_KEYWORD, mod.getKeyword()); + assertEquals("Incorrect position", 18, mod.getStartPosition()); + assertEquals("Incorrect content", "module", contents.substring(mod.getStartPosition(), mod.getStartPosition()+6)); + assertEquals("Incorrect name", "java.base", imp.getName().toString()); + } + { + ImportDeclaration imp = imports.get(1); + assertEquals("Incorrect modifier bits", Modifier.STATIC, imp.getModifiers()); + assertEquals("Incorrect no of modifiers", 1, imp.modifiers().size()); + Modifier mod = (Modifier) imp.modifiers().get(0); + assertEquals("Incorrect modifier", "static", mod.getKeyword().toString()); + assertEquals("Incorrect modifier", Modifier.ModifierKeyword.STATIC_KEYWORD, mod.getKeyword()); + assertEquals("Incorrect position", 43, mod.getStartPosition()); + assertEquals("Incorrect content", "static", contents.substring(mod.getStartPosition(), mod.getStartPosition()+6)); + assertEquals("Incorrect name", "java.lang.System.out", imp.getName().toString()); + } + } +} diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTTest.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTTest.java index c503fe069ad..92a017a729d 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTTest.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTTest.java @@ -87,6 +87,7 @@ import org.eclipse.jdt.core.dom.MethodRef; import org.eclipse.jdt.core.dom.MethodRefParameter; import org.eclipse.jdt.core.dom.Modifier; +import org.eclipse.jdt.core.dom.Modifier.ModifierKeyword; import org.eclipse.jdt.core.dom.ModuleDeclaration; import org.eclipse.jdt.core.dom.Name; import org.eclipse.jdt.core.dom.NormalAnnotation; @@ -151,6 +152,13 @@ public class ASTTest extends org.eclipse.jdt.core.tests.junit.extension.TestCase */ protected static final int AST_INTERNAL_JLS9 = AST.JLS9; + /** + * Internal synonym for constant AST.JSL9 + * to alleviate deprecation warnings once AST.JLS9 is deprecated in future. + * @deprecated + */ + protected static final int AST_INTERNAL_JLS23 = AST.JLS23; + class CheckPositionsMatcher extends ASTMatcher { public CheckPositionsMatcher() { @@ -862,6 +870,7 @@ public static Test suite() { suite.addTest(new ASTTest(methods[i].getName(), JLS3_INTERNAL)); suite.addTest(new ASTTest(methods[i].getName(), AST.JLS4)); suite.addTest(new ASTTest(methods[i].getName(), getJLS8())); + suite.addTest(new ASTTest(methods[i].getName(), AST_INTERNAL_JLS23)); } } return suite; @@ -2734,7 +2743,19 @@ public void set(ASTNode value) { assertTrue(this.ast.modificationCount() > previousCount); assertTrue(x.isOnDemand() == true); - if (this.ast.apiLevel() >= JLS3_INTERNAL) { + if (this.ast.apiLevel() >= AST_INTERNAL_JLS23) { + Modifier mod = this.ast.newModifier(ModifierKeyword.STATIC_KEYWORD); + x.modifiers().add(mod); + assertTrue(this.ast.modificationCount() > previousCount); + assertTrue(x.isStatic() == true); + previousCount = this.ast.modificationCount(); + x.modifiers().clear(); + mod = this.ast.newModifier(ModifierKeyword.MODULE_KEYWORD); + x.modifiers().add(mod); + assertTrue(this.ast.modificationCount() > previousCount); + assertTrue(x.modifiers().size() == 1); + assertEquals(((Modifier) x.modifiers().get(0)).getKeyword(), ModifierKeyword.MODULE_KEYWORD); + } else if (this.ast.apiLevel() >= JLS3_INTERNAL) { x.setStatic(true); assertTrue(this.ast.modificationCount() > previousCount); assertTrue(x.isStatic() == true); @@ -6643,7 +6664,11 @@ public void testSwitchCase() { assertTrue(x.getParent() == null); assertTrue(x.getExpression().getParent() == x); assertTrue(x.getLeadingComment() == null); - assertTrue(!x.isDefault()); + if (this.ast.apiLevel() < AST_INTERNAL_JLS23) { + assertTrue(!x.isDefault()); + } else { + assertEquals(0, x.expressions().size()); + } assertTrue(x.getNodeType() == ASTNode.SWITCH_CASE); assertTrue(x.structuralPropertiesForType() == SwitchCase.propertyDescriptors(this.ast.apiLevel())); diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/RunConverterTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/RunConverterTests.java index 3acb52c6aa7..d0e252794bf 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/RunConverterTests.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/RunConverterTests.java @@ -58,6 +58,7 @@ public static Class[] getAllTestClasses() { ASTConverter_15Test.class, ASTConverter_16Test.class, ASTConverter_17Test.class, + ASTConverter_23Test.class, ASTConverter_GuardedPattern_Test.class, ASTConverter_RecordPattern_Test.class, ASTConverterSuperAfterStatements.class, diff --git a/org.eclipse.jdt.core.tests.model/workspace/Converter_23/.classpath b/org.eclipse.jdt.core.tests.model/workspace/Converter_23/.classpath index 6524f7b9eed..95b37bfb0a6 100644 --- a/org.eclipse.jdt.core.tests.model/workspace/Converter_23/.classpath +++ b/org.eclipse.jdt.core.tests.model/workspace/Converter_23/.classpath @@ -1,6 +1,10 @@ - + + + + + diff --git a/org.eclipse.jdt.core/.settings/.api_filters b/org.eclipse.jdt.core/.settings/.api_filters index c156be3e20c..444ece9cff2 100644 --- a/org.eclipse.jdt.core/.settings/.api_filters +++ b/org.eclipse.jdt.core/.settings/.api_filters @@ -192,6 +192,12 @@ + + + + + + @@ -251,6 +257,14 @@ + + + + + + + + diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/AST.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/AST.java index 687dcc62496..42a00650630 100644 --- a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/AST.java +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/AST.java @@ -2499,6 +2499,9 @@ public List newModifiers(int flags) { if (Modifier.isNonSealed(flags)) { result.add(newModifier(Modifier.ModifierKeyword.NON_SEALED_KEYWORD)); } + if (Modifier.isModule(flags)) { + result.add(newModifier(Modifier.ModifierKeyword.MODULE_KEYWORD)); + } return result; } diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ASTConverter.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ASTConverter.java index ffb1d888eec..71d485ee447 100644 --- a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ASTConverter.java +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ASTConverter.java @@ -3687,16 +3687,27 @@ public ImportDeclaration convertImport(org.eclipse.jdt.internal.compiler.ast.Imp importDeclaration.setOnDemand(onDemand); int modifiers = importReference.modifiers; if (modifiers != ClassFileConstants.AccDefault) { - switch(this.ast.apiLevel) { - case AST.JLS2_INTERNAL : + if (this.ast.apiLevel == AST.JLS2_INTERNAL) { + importDeclaration.setFlags(importDeclaration.getFlags() | ASTNode.MALFORMED); + } else if (this.ast.apiLevel < AST.JLS23_INTERNAL) { + if (modifiers == ClassFileConstants.AccStatic) { + importDeclaration.setStatic(true); + } else { importDeclaration.setFlags(importDeclaration.getFlags() | ASTNode.MALFORMED); - break; - default : - if (modifiers == ClassFileConstants.AccStatic) { - importDeclaration.setStatic(true); - } else { - importDeclaration.setFlags(importDeclaration.getFlags() | ASTNode.MALFORMED); - } + } + } else { + ModifierKeyword keyword = switch (modifiers) { + case ClassFileConstants.AccStatic -> ModifierKeyword.STATIC_KEYWORD; + case ClassFileConstants.AccModule -> ModifierKeyword.MODULE_KEYWORD; + default -> null; + }; + if (keyword != null) { + Modifier newModifier = this.ast.newModifier(keyword); + newModifier.setSourceRange(importReference.modifiersSourceStart, keyword.toString().length()); + importDeclaration.modifiers().add(newModifier); + } else { + importDeclaration.setFlags(importDeclaration.getFlags() | ASTNode.MALFORMED); + } } } if (this.resolveBindings) { diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ASTMatcher.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ASTMatcher.java index 2c2ee64eb92..b441329a389 100644 --- a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ASTMatcher.java +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ASTMatcher.java @@ -1129,7 +1129,11 @@ public boolean match(ImportDeclaration node, Object other) { return false; } ImportDeclaration o = (ImportDeclaration) other; - if (node.getAST().apiLevel >= AST.JLS3_INTERNAL) { + if (node.getAST().apiLevel >= AST.JLS23_INTERNAL) { + if (!safeSubtreeListMatch(node.modifiers(), o.modifiers())) { + return false; + } + } else if (node.getAST().apiLevel >= AST.JLS3_INTERNAL) { if (node.isStatic() != o.isStatic()) { return false; } diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ImportDeclaration.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ImportDeclaration.java index afcf974e82e..f6d46fa3c57 100644 --- a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ImportDeclaration.java +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ImportDeclaration.java @@ -17,6 +17,8 @@ import java.util.ArrayList; import java.util.List; +import org.eclipse.jdt.core.dom.Modifier.ModifierKeyword; + /** * Import declaration AST node type. * @@ -51,6 +53,14 @@ public class ImportDeclaration extends ASTNode { public static final SimplePropertyDescriptor STATIC_PROPERTY = new SimplePropertyDescriptor(ImportDeclaration.class, "static", boolean.class, MANDATORY); //$NON-NLS-1$ + /** + * The "modifiers" structural property of this node type (element type: {@link IExtendedModifier}) (added in JLS23 API). + * @since 3.39 + * @noreference preview feature + */ + public static final ChildListPropertyDescriptor MODIFIERS_PROPERTY = + new ChildListPropertyDescriptor(ImportDeclaration.class, "modifiers", IExtendedModifier.class, CYCLE_RISK); //$NON-NLS-1$ + /** * A list of property descriptors (element type: * {@link StructuralPropertyDescriptor}), @@ -67,6 +77,14 @@ public class ImportDeclaration extends ASTNode { */ private static final List PROPERTY_DESCRIPTORS_3_0; + /** + * A list of property descriptors (element type: + * {@link StructuralPropertyDescriptor}), + * or null if uninitialized. + * @since 3.39 + */ + private static final List PROPERTY_DESCRIPTORS_23; + static { List properyList = new ArrayList(3); createPropertyList(ImportDeclaration.class, properyList); @@ -80,6 +98,14 @@ public class ImportDeclaration extends ASTNode { addProperty(NAME_PROPERTY, properyList); addProperty(ON_DEMAND_PROPERTY, properyList); PROPERTY_DESCRIPTORS_3_0 = reapPropertyList(properyList); + + properyList = new ArrayList(5); + createPropertyList(ImportDeclaration.class, properyList); + addProperty(STATIC_PROPERTY, properyList); + addProperty(MODIFIERS_PROPERTY, properyList); + addProperty(NAME_PROPERTY, properyList); + addProperty(ON_DEMAND_PROPERTY, properyList); + PROPERTY_DESCRIPTORS_23 = reapPropertyList(properyList); } /** @@ -94,10 +120,12 @@ public class ImportDeclaration extends ASTNode { * @since 3.0 */ public static List propertyDescriptors(int apiLevel) { - if (apiLevel == AST.JLS2_INTERNAL) { - return PROPERTY_DESCRIPTORS_2_0; - } else { + if (apiLevel >= AST.JLS23_INTERNAL) { + return PROPERTY_DESCRIPTORS_23; + } else if (apiLevel >= AST.JLS3_INTERNAL) { return PROPERTY_DESCRIPTORS_3_0; + } else { + return PROPERTY_DESCRIPTORS_2_0; } } @@ -119,6 +147,14 @@ public static List propertyDescriptors(int apiLevel) { */ private boolean isStatic = false; + /** + * The extended modifiers (element type: {@link IExtendedModifier}). + * Added in JLS23; defaults to an empty list + * (see constructor). + * @since 3.39 + */ + private ASTNode.NodeList modifiers = null; + /** * Creates a new AST node for an import declaration owned by the * given AST. The import declaration initially is a regular (non-static) @@ -133,6 +169,55 @@ public static List propertyDescriptors(int apiLevel) { */ ImportDeclaration(AST ast) { super(ast); + if (ast.apiLevel >= AST.JLS23_INTERNAL) { + this.modifiers = new ASTNode.NodeList(MODIFIERS_PROPERTY); + } + } + + /** + * Returns the live ordered list of modifiers of this declaration (added in JLS23 API). + * + * @return the live list of modifiers (element type: {@link IExtendedModifier}) + * @exception UnsupportedOperationException if this operation is used in + * an AST below JLS23 + * @since 3.39 + */ + public List modifiers() { + if (this.ast.apiLevel < AST.JLS23_INTERNAL) + throw new UnsupportedOperationException("Operation not supported in AST below JLS23"); //$NON-NLS-1$ + return this.modifiers; + } + + /** + * Returns the modifiers explicitly specified on this declaration. + *

+ * This method is a convenience method that computes these flags based on availability: + *

+ *
    + *
  • At JLS23 it is computed from from {@link #modifiers()}.
  • + *
  • At JLS3 only the information from {@link #isStatic()} is available.
  • + *
  • At lower JLS {@code 0} is constantly returned. + *
+ * + * @return the bit-wise "or" of Modifier constants + * @see Modifier + * @since 3.39 + */ + public int getModifiers() { + if (this.modifiers == null) { + // JLS3 behavior (for JLS2 this is constantly 0) + return this.isStatic ? Modifier.STATIC : Modifier.NONE; + } + // JLS23 behavior - convenience method + // performance could be improved by caching computed flags + // but this would require tracking changes to this.modifiers + int computedmodifierFlags = Modifier.NONE; + for (Object x : modifiers()) { + if (x instanceof Modifier modifier) { + computedmodifierFlags |= modifier.getKeyword().toFlagValue(); + } + } + return computedmodifierFlags; } @Override @@ -176,11 +261,21 @@ final ASTNode internalGetSetChildProperty(ChildPropertyDescriptor property, bool return super.internalGetSetChildProperty(property, get, child); } + @Override + final List internalGetChildListProperty(ChildListPropertyDescriptor property) { + if (property == MODIFIERS_PROPERTY) { + return modifiers(); + } + // allow default implementation to flag the error + return super.internalGetChildListProperty(property); + } + @Override final int getNodeType0() { return IMPORT_DECLARATION; } + @SuppressWarnings("unchecked") @Override ASTNode clone0(AST target) { ImportDeclaration result = new ImportDeclaration(target); @@ -189,6 +284,9 @@ ASTNode clone0(AST target) { if (this.ast.apiLevel >= AST.JLS3_INTERNAL) { result.setStatic(isStatic()); } + if (this.ast.apiLevel >= AST.JLS23_INTERNAL) { + result.modifiers().addAll(ASTNode.copySubtrees(target, modifiers())); + } result.setName((Name) getName().clone(target)); return result; } @@ -204,6 +302,9 @@ void accept0(ASTVisitor visitor) { boolean visitChildren = visitor.visit(this); if (visitChildren) { acceptChild(visitor, getName()); + if (this.ast.apiLevel >= AST.JLS23_INTERNAL) { + acceptChildren(visitor, this.modifiers); + } } visitor.endVisit(this); } @@ -296,6 +397,15 @@ public void setOnDemand(boolean onDemand) { * @since 3.1 */ public boolean isStatic() { + if (this.modifiers != null) { + // JLS23 behavior: extract from the list of Modifier + for (Object x : modifiers()) { + if (x instanceof Modifier modifier && modifier.isStatic()) { + return true; + } + } + return false; + } unsupportedIn2(); return this.isStatic; } @@ -303,13 +413,35 @@ public boolean isStatic() { /** * Sets whether this import declaration is a static import (added in JLS3 API). * + * Note, that in JLS23 API this method creates a {@link Modifier} without source positions + * (or removes, if {@code isStatic == false}), so it should not be invoked in situations where + * valid source positions are required. + * * @param isStatic true if this is a static import, * and false if this is a regular import * @exception UnsupportedOperationException if this operation is used in * a JLS2 AST * @since 3.1 + * + * @see #modifiers() */ + @SuppressWarnings("unchecked") public void setStatic(boolean isStatic) { + if (this.ast.apiLevel >= AST.JLS23_INTERNAL) { + List mods = modifiers(); + for (Modifier mod : mods) { + if (mod.isStatic()) { + if (!isStatic) + this.modifiers.remove(mod); + return; + } + } + if (isStatic) { + Modifier newMod = this.ast.newModifier(ModifierKeyword.STATIC_KEYWORD); + this.modifiers.add(newMod); + } + return; + } unsupportedIn2(); preValueChange(STATIC_PROPERTY); this.isStatic = isStatic; diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/Modifier.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/Modifier.java index f7d1c62cb3e..1cc5704b999 100644 --- a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/Modifier.java +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/Modifier.java @@ -120,6 +120,12 @@ public static class ModifierKeyword { */ public static final ModifierKeyword NON_SEALED_KEYWORD = new ModifierKeyword("non-sealed", NON_SEALED);//$NON-NLS-1$ + /** + * @since 3.39 + * @noreference preview feature + */ + public static final ModifierKeyword MODULE_KEYWORD = new ModifierKeyword("module", MODULE);//$NON-NLS-1$ + static { KEYWORDS = new HashMap(20); ModifierKeyword[] ops = { @@ -136,7 +142,8 @@ public static class ModifierKeyword { STRICTFP_KEYWORD, DEFAULT_KEYWORD, SEALED_KEYWORD, - NON_SEALED_KEYWORD + NON_SEALED_KEYWORD, + MODULE_KEYWORD }; for (ModifierKeyword op : ops) { KEYWORDS.put(op.toString(), op); @@ -348,6 +355,13 @@ public String toString() { * @noreference preview feature */ public static final int WHEN = 0x2000; + /** + * "module" modifier constant (bit mask). + * Applicable only to imports. + * @since 3.39 + * @noreference preview feature + */ + public static final int MODULE = 0x8000; /** * "default" modifier constant (bit mask) (added in JLS8 API). @@ -788,6 +802,16 @@ public boolean isNonSealed() { return this.modifierKeyword == ModifierKeyword.NON_SEALED_KEYWORD; } + /** + * Answer true if the receiver is the module modifier, false otherwise. + * + * @return true if the receiver is the module modifier, false otherwise + * @since 3.39 + */ + public static boolean isModule(int flags) { + return (flags & Modifier.MODULE) != 0; + } + @Override int memSize() { // treat ModifierKeyword as free diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/NaiveASTFlattener.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/NaiveASTFlattener.java index 11afb189299..80e5ddc2017 100644 --- a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/NaiveASTFlattener.java +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/NaiveASTFlattener.java @@ -95,6 +95,13 @@ public class NaiveASTFlattener extends ASTVisitor { */ private static final int JLS21 = AST.JLS21; + /** + * Internal synonym for {@link AST#JLS23}. Use to alleviate + * deprecation warnings. + * @deprecated + */ + private static final int JLS23 = AST.JLS23; + /** * The string buffer into which the serialized representation of the AST is * written. @@ -845,7 +852,11 @@ public boolean visit(IfStatement node) { public boolean visit(ImportDeclaration node) { printIndent(); this.buffer.append("import ");//$NON-NLS-1$ - if (node.getAST().apiLevel() >= JLS3) { + if (node.getAST().apiLevel() >= JLS23) { + if (node.modifiers().size() == 1) { + this.buffer.append(((Modifier) node.modifiers().get(0)).getKeyword().toString()).append(' '); + } + } else if (node.getAST().apiLevel() >= JLS3) { if (node.isStatic()) { this.buffer.append("static ");//$NON-NLS-1$ } diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/ASTRewriteFlattener.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/ASTRewriteFlattener.java index a612e3e9a7b..256df95e64a 100644 --- a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/ASTRewriteFlattener.java +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/ASTRewriteFlattener.java @@ -98,6 +98,9 @@ public class ASTRewriteFlattener extends ASTVisitor { /** @deprecated using deprecated code */ private static final int JLS14_INTERNAL = AST.JLS14; + /** @deprecated using deprecated code */ + private static final int JLS23_INTERNAL = AST.JLS23; + public static String asString(ASTNode node, RewriteEventStore store) { ASTRewriteFlattener flattener= new ASTRewriteFlattener(store); node.accept(flattener); @@ -603,7 +606,12 @@ public boolean visit(IfStatement node) { @Override public boolean visit(ImportDeclaration node) { this.result.append("import "); //$NON-NLS-1$ - if (node.getAST().apiLevel() >= JLS3_INTERNAL) { + if (node.getAST().apiLevel() >= JLS23_INTERNAL) { + List modifiers = node.modifiers(); + for (Modifier modifier : modifiers) { + this.result.append(modifier.getKeyword().toString()).append(' '); + } + } else if (node.getAST().apiLevel() >= JLS3_INTERNAL) { if (getBooleanAttribute(node, ImportDeclaration.STATIC_PROPERTY)) { this.result.append("static ");//$NON-NLS-1$ } diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/compiler/SourceElementParser.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/compiler/SourceElementParser.java index 9e33bd40a88..45594c8948b 100644 --- a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/compiler/SourceElementParser.java +++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/compiler/SourceElementParser.java @@ -617,42 +617,9 @@ protected void consumeSingleMemberAnnotation(boolean isTypeAnnotation) { @Override protected void consumeSingleStaticImportDeclarationName() { // SingleTypeImportDeclarationName ::= 'import' 'static' Name - ImportReference impt; - int length; - char[][] tokens = new char[length = this.identifierLengthStack[this.identifierLengthPtr--]][]; - this.identifierPtr -= length; - long[] positions = new long[length]; - System.arraycopy(this.identifierStack, this.identifierPtr + 1, tokens, 0, length); - System.arraycopy(this.identifierPositionStack, this.identifierPtr + 1, positions, 0, length); - pushOnAstStack(impt = newImportReference(tokens, positions, false, ClassFileConstants.AccStatic)); - - this.modifiers = ClassFileConstants.AccDefault; - this.modifiersSourceStart = -1; // <-- see comment into modifiersFlag(int) + super.consumeSingleStaticImportDeclarationName(); - if (this.currentToken == TokenNameSEMICOLON){ - impt.declarationSourceEnd = this.scanner.currentPosition - 1; - } else { - impt.declarationSourceEnd = impt.sourceEnd; - } - impt.declarationEnd = impt.declarationSourceEnd; - //this.endPosition is just before the ; - impt.declarationSourceStart = this.intStack[this.intPtr--]; - - if(!this.statementRecoveryActivated && - this.options.sourceLevel < ClassFileConstants.JDK1_5 && - this.lastErrorEndPositionBeforeRecovery < this.scanner.currentPosition) { - impt.modifiers = ClassFileConstants.AccDefault; // convert the static import reference to a non-static importe reference - problemReporter().invalidUsageOfStaticImports(impt); - } - - // recovery - if (this.currentElement != null){ - this.lastCheckPoint = impt.declarationSourceEnd+1; - this.currentElement = this.currentElement.add(impt, 0); - this.lastIgnoredToken = -1; - this.restartRecovery = true; // used to avoid branching back into the regular automaton - } - if (this.reportReferenceInfo) { + if (this.reportReferenceInfo && this.astStack[this.astPtr] instanceof ImportReference impt) { // Name for static import is TypeName '.' Identifier // => accept unknown ref on identifier int tokensLength = impt.tokens.length-1; @@ -714,44 +681,8 @@ protected void consumeStaticImportOnDemandDeclarationName() { /* push an ImportRef build from the last name stored in the identifier stack. */ - ImportReference impt; - int length; - char[][] tokens = new char[length = this.identifierLengthStack[this.identifierLengthPtr--]][]; - this.identifierPtr -= length; - long[] positions = new long[length]; - System.arraycopy(this.identifierStack, this.identifierPtr + 1, tokens, 0, length); - System.arraycopy(this.identifierPositionStack, this.identifierPtr + 1, positions, 0, length); - pushOnAstStack(impt = new ImportReference(tokens, positions, true, ClassFileConstants.AccStatic)); - - // star end position - impt.trailingStarPosition = this.intStack[this.intPtr--]; - this.modifiers = ClassFileConstants.AccDefault; - this.modifiersSourceStart = -1; // <-- see comment into modifiersFlag(int) - - if (this.currentToken == TokenNameSEMICOLON){ - impt.declarationSourceEnd = this.scanner.currentPosition - 1; - } else { - impt.declarationSourceEnd = impt.sourceEnd; - } - impt.declarationEnd = impt.declarationSourceEnd; - //this.endPosition is just before the ; - impt.declarationSourceStart = this.intStack[this.intPtr--]; - - if(!this.statementRecoveryActivated && - this.options.sourceLevel < ClassFileConstants.JDK1_5 && - this.lastErrorEndPositionBeforeRecovery < this.scanner.currentPosition) { - impt.modifiers = ClassFileConstants.AccDefault; // convert the static import reference to a non-static importe reference - problemReporter().invalidUsageOfStaticImports(impt); - } - - // recovery - if (this.currentElement != null){ - this.lastCheckPoint = impt.declarationSourceEnd+1; - this.currentElement = this.currentElement.add(impt, 0); - this.lastIgnoredToken = -1; - this.restartRecovery = true; // used to avoid branching back into the regular automaton - } - if (this.reportReferenceInfo) { + super.consumeStaticImportOnDemandDeclarationName(); + if (this.reportReferenceInfo && this.astStack[this.astPtr] instanceof ImportReference impt) { this.requestor.acceptTypeReference(impt.tokens, impt.sourceStart, impt.sourceEnd); } } From f08e389b6e03cb9de214058d10b77935c757c313 Mon Sep 17 00:00:00 2001 From: Stephan Herrmann Date: Tue, 20 Aug 2024 21:08:41 +0200 Subject: [PATCH 4/4] [23] Selection for module imports (#2839) + implement selection for module imports + includes new ResolveTests23 + also add forgotten ResolveTests21 to the suite - but remove tests for withdrawn string template feature fixes https://github.com/eclipse-jdt/eclipse.jdt.core/issues/2838 --- .../core/tests/model/AllJavaModelTests.java | 2 + .../jdt/core/tests/model/ResolveTests21.java | 103 ----------- .../jdt/core/tests/model/ResolveTests23.java | 173 ++++++++++++++++++ .../internal/codeassist/SelectionEngine.java | 13 +- .../codeassist/impl/AssistParser.java | 5 +- 5 files changed, 189 insertions(+), 107 deletions(-) create mode 100644 org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests23.java diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java index 43eac170bed..51f54913912 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java @@ -117,6 +117,8 @@ private static Class[] getAllTestClasses() { ResolveTests9.class, ResolveTests10.class, ResolveTests12To15.class, + ResolveTests21.class, + ResolveTests23.class, SelectionJavadocModelTests.class, // Some test suite above breaks completion tests below diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests21.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests21.java index 8fd5dddd65a..e8c00d054c9 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests21.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests21.java @@ -65,109 +65,6 @@ protected void tearDown() throws Exception { super.tearDown(); } -public void test001() throws JavaModelException { - this.wc = getWorkingCopy("/Resolve/src/X.java", - "public class X {\n" - + " private String abc = \"abc\"; // unused\n" - + " public void main(String[] args) {\n" - + " String s = STR.\"A simple String \\{clone(abc)}\";\n" - + " System.out.println(s);\n" - + " }\n" - + " public String clone(String s) {\n" - + " return \"clone\";\n" - + " }\n" - + "}"); - String str = this.wc.getSource(); - String selection = "clone"; - int start = str.indexOf(selection); - int length = selection.length(); - IJavaElement[] elements = this.wc.codeSelect(start, length); - assertElementsEqual( - "Unexpected elements", - "clone(String) [in X [in [Working copy] X.java [in [in src [in Resolve]]]]]", - elements - ); -} -public void test002() throws JavaModelException { - this.wc = getWorkingCopy("/Resolve/src/X.java", - "public class X {\n" - + " private String abc = \"abc\"; // unused\n" - + " public void main(String[] args) {\n" - + " String s = STR.\"A simple String \\{clone(abc)}\";\n" - + " System.out.println(s);\n" - + " }\n" - + " public String clone(String s) {\n" - + " return \"clone\";\n" - + " }\n" - + "}"); - String str = this.wc.getSource(); - String selection = "abc"; - int start = str.lastIndexOf(selection); - int length = selection.length(); - IJavaElement[] elements = this.wc.codeSelect(start, length); - assertElementsEqual( - "Unexpected elements", - "abc [in X [in [Working copy] X.java [in [in src [in Resolve]]]]]", - elements - ); -} -public void test003() throws JavaModelException { - this.wc = getWorkingCopy("/Resolve/src/X.java", - "public class X {\n" - + " static int CONST = 0;\n" - + " private static int foo() {\n" - + " return CONST;\n" - + " }\n" - + " public static void main(String argv[]) {\n" - + " String str = STR.\"{\\{new Object() { class Test { int i; Test() { i = foo();}}}.new Test().i\\u007d}\";\n" - + " System.out.println(str.equals(\"{0}\"));\n" - + " }\n" - + "}"); - String str = this.wc.getSource(); - String selection = "foo"; - int start = str.lastIndexOf(selection); - int length = selection.length(); - IJavaElement[] elements = this.wc.codeSelect(start, length); - assertElementsEqual( - "Unexpected elements", - "foo() [in X [in [Working copy] X.java [in [in src [in Resolve]]]]]", - elements - ); -} -public void test004() throws JavaModelException { - this.wc = getWorkingCopy("/Resolve/src/X.java", - "public class X {\n" - + " private final static int LF = (char) 0x000A;\n" - + " private static boolean compare(String s) {\n" - + " char[] chars = new char[] {LF,'a','b','c','d'};\n" - + " if (chars.length != s.length())\n" - + " return false;\n" - + " for (int i = 0; i < s.length(); i++) {\n" - + " if(chars[i] != s.charAt(i)) {\n" - + " return false;\n" - + " }\n" - + " }\n" - + " return true;\n" - + " }\n" - + " public static void main(String argv[]) {\n" - + " String abcd = \"abcd\"; //$NON-NLS-1$\n" - + " String textBlock = STR.\"\"\"\n" - + " \n" - + "\\{abcd}\"\"\";//$NON-NLS-1$\n" - + " System.out.println(compare(textBlock));\n" - + " }\n" - + "}"); - String str = this.wc.getSource(); - String selection = "abcd"; - int start = str.lastIndexOf(selection); - int length = selection.length(); - IJavaElement[] elements = this.wc.codeSelect(start, length); - assertElementsEqual( - "Unexpected elements", - "abcd [in main(String[]) [in X [in [Working copy] X.java [in [in src [in Resolve]]]]]]", - elements - ); -} // https://github.com/eclipse-jdt/eclipse.jdt.core/issues/2572 // [code select] ClassCastException when hovering in switch case yield public void testIssue2572() throws JavaModelException { diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests23.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests23.java new file mode 100644 index 00000000000..4cf88e91429 --- /dev/null +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ResolveTests23.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright (c) 2024 GK Software SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * This is an implementation of an early-draft specification developed under the Java + * Community Process (JCP) and is made available for testing and evaluation purposes + * only. The code is not compatible with any specification of the JCP. + * + * Contributors: + * Stephan herrmann - initial implementation + *******************************************************************************/ + +package org.eclipse.jdt.core.tests.model; + +import java.io.IOException; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.WorkingCopyOwner; + +import junit.framework.Test; + +public class ResolveTests23 extends AbstractJavaModelTests { + ICompilationUnit wc = null; + +static { +// TESTS_NAMES = new String[] { "testModuleImport3_firstSegment" }; + // TESTS_NUMBERS = new int[] { 124 }; + // TESTS_RANGE = new int[] { 16, -1 }; +} +public static Test suite() { + return buildModelTestSuite(ResolveTests23.class); +} +public ResolveTests23(String name) { + super(name); +} +@Override +public ICompilationUnit getWorkingCopy(String path, String source) throws JavaModelException { + return super.getWorkingCopy(path, source, this.wcOwner); +} +@Override +public void setUpSuite() throws Exception { + super.setUpSuite(); + this.currentProject = setUpJavaProject("Resolve", "23", false); + this.currentProject.setOption(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.ENABLED); + waitUntilIndexesReady(); +} +@Override +protected void setUp() throws Exception { + super.setUp(); + this.wcOwner = new WorkingCopyOwner(){}; +} +@Override +public void tearDownSuite() throws Exception { + deleteProject("Resolve"); + super.tearDownSuite(); +} + +@Override +protected void tearDown() throws Exception { + if (this.wc != null) { + this.wc.discardWorkingCopy(); + } + super.tearDown(); +} + +public void testModuleImport1() throws IOException, CoreException { + String jarName = "mod.one.jar"; + addModularLibrary(this.currentProject, jarName, "mod.one.zip", + new String[] { + "module-info.java", + "module mod.one {}" + }, + "23"); + try { + this.wc = getWorkingCopy("/Resolve/src/p/X.java", + """ + package p; + import module mod.one; + class X{} + """); + String str = this.wc.getSource(); + String selection = "one"; + int start = str.indexOf(selection); + int length = selection.length(); + IJavaElement[] elements = this.wc.codeSelect(start, length); + assertElementsEqual( + "Unexpected elements", + "mod.one [in module-info.class [in [in mod.one.jar [in Resolve]]]]", + elements + ); + } finally { + removeLibrary(this.currentProject, jarName, null); + } +} +public void testModuleImport2_prefix() throws IOException, CoreException { + String jarName = "mod.one.jar"; + addModularLibrary(this.currentProject, jarName, "mod.one.zip", + new String[] { + "module-info.java", + "module mod.one {}" + }, + "23"); + try { + this.wc = getWorkingCopy("/Resolve/src/p/X.java", + """ + package p; + import module mod.o; + class X{} + """); + String str = this.wc.getSource(); + String selection = "mod.o"; + int start = str.indexOf(selection); + int length = selection.length(); + IJavaElement[] elements = this.wc.codeSelect(start, length); + assertElementsEqual( + "Unexpected elements", + "mod.one [in module-info.class [in [in mod.one.jar [in Resolve]]]]", + elements + ); + } finally { + removeLibrary(this.currentProject, jarName, null); + } +} +public void testModuleImport3_firstSegment() throws IOException, CoreException { + // assert that selecting only the first segment of a module name still uses the full name for module lookup + String jarName1 = "mod.one.jar"; + addModularLibrary(this.currentProject, jarName1, "mod.one.zip", + new String[] { + "module-info.java", + "module mod.one {}" + }, + "23"); + // second module for potential ambiguity of name part "mod": + String jarName2 = "mod.two.jar"; + addModularLibrary(this.currentProject, jarName2, "mod.two.zip", + new String[] { + "module-info.java", + "module mod.two {}" + }, + "23"); + try { + this.wc = getWorkingCopy("/Resolve/src/p/X.java", + """ + package p; + import module mod.one; + class X{} + """); + String str = this.wc.getSource(); + String selection = "mod.one"; + int start = str.indexOf(selection); + int length = "mod".length(); // <<== only first segment + IJavaElement[] elements = this.wc.codeSelect(start, length); + assertElementsEqual( + "Unexpected elements", + "mod.one [in module-info.class [in [in mod.one.jar [in Resolve]]]]", + elements + ); + } finally { + removeLibrary(this.currentProject, jarName1, null); + removeLibrary(this.currentProject, jarName2, null); + } +} +} diff --git a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/SelectionEngine.java b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/SelectionEngine.java index 7231ad77150..55b2b5b1df2 100644 --- a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/SelectionEngine.java +++ b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/SelectionEngine.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2021 IBM Corporation and others. + * Copyright (c) 2000, 2024 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -8,6 +8,10 @@ * * SPDX-License-Identifier: EPL-2.0 * + * This is an implementation of an early-draft specification developed under the Java + * Community Process (JCP) and is made available for testing and evaluation purposes + * only. The code is not compatible with any specification of the JCP. + * * Contributors: * IBM Corporation - initial API and implementation * Jesper Steen Møller - contributions for: @@ -1013,6 +1017,10 @@ public void select( if (importReference instanceof SelectionOnImportReference) { char[][] tokens = ((SelectionOnImportReference) importReference).tokens; this.noProposal = false; + if ((importReference.modifiers & ClassFileConstants.AccModule) != 0 && this.compilerOptions.enablePreviewFeatures) { + this.nameEnvironment.findModules(CharOperation.concatWithAll(tokens, '.'), this, null); + return; + } this.requestor.acceptPackage(CharOperation.concatWith(tokens, '.')); this.nameEnvironment.findTypes(CharOperation.concatWith(tokens, '.'), false, false, IJavaSearchConstants.TYPE, this); @@ -2059,7 +2067,6 @@ private Object visitInheritDocInterfaces(ArrayList visited, ReferenceBinding cur @Override public void acceptModule(char[] moduleName) { - // TODO Auto-generated method stub - + this.requestor.acceptModule(moduleName, moduleName, this.actualSelectionStart, this.actualSelectionEnd); } } diff --git a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/impl/AssistParser.java b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/impl/AssistParser.java index 6084b8807b3..f94966b95ff 100644 --- a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/impl/AssistParser.java +++ b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/impl/AssistParser.java @@ -1091,7 +1091,10 @@ protected void consumeSingleModifierImportDeclarationName(int modifier) { /* retrieve identifiers subset and whole positions, the assist node positions should include the entire replaced source. */ int length = this.identifierLengthStack[this.identifierLengthPtr]; - char[][] subset = identifierSubSet(index+1); // include the assistIdentifier + int subsetLength = modifier == ClassFileConstants.AccStatic + ? index + 1 // include the assistIdentifier + : length; // segments of a module name have no semantic relevance, use the entire name + char[][] subset = identifierSubSet(subsetLength); this.identifierLengthPtr--; this.identifierPtr -= length; long[] positions = new long[length];