diff --git a/ide/jumpto/src/org/netbeans/modules/jumpto/symbol/ContentProviderImpl.java b/ide/jumpto/src/org/netbeans/modules/jumpto/symbol/ContentProviderImpl.java index a01b32ad6557..8f56ad3e51e5 100644 --- a/ide/jumpto/src/org/netbeans/modules/jumpto/symbol/ContentProviderImpl.java +++ b/ide/jumpto/src/org/netbeans/modules/jumpto/symbol/ContentProviderImpl.java @@ -391,8 +391,6 @@ public void run() { if (done || resultChanged) { lastSize = newSize[0]; lastProvCount = newProvCount; - model.remove(toRemove); - model.add(toAdd); attrCopier.checkWrongCase(toRemove, toAdd); if ( isCanceled ) { LOG.log( @@ -405,6 +403,17 @@ public void run() { Level.FINE, "Worker for text {0} finished after {1} ms.", //NOI18N new Object[]{text, System.currentTimeMillis() - createTime}); + /* NB: model.remove and model.add must happen inside + the invokeLater runnable, guarded by !isCanceled, + so that + (a) the remove and add are a single EDT turn (no + intervening repaint/keystroke between them), + and + (b) a Worker that was cancelled after + getSymbolNames cannot mutate the live model + one final time. The cancel flag is set from + the EDT in setListModel, so checking it on + the EDT here is race-free. */ SwingUtilities.invokeLater(() -> { if (done) { final Pair nameAndScope = Utils.splitNameAndScope(text); @@ -418,6 +427,8 @@ public void run() { nameAndScope.second()); } if (!isCanceled) { + model.remove(toRemove); + model.add(toAdd); enableOK(panel.setModel(model, done)); } }); diff --git a/java/java.sourceui/src/org/netbeans/modules/java/source/ui/AsyncJavaSymbolDescriptor.java b/java/java.sourceui/src/org/netbeans/modules/java/source/ui/AsyncJavaSymbolDescriptor.java index 2254f7a47850..7a041922806d 100644 --- a/java/java.sourceui/src/org/netbeans/modules/java/source/ui/AsyncJavaSymbolDescriptor.java +++ b/java/java.sourceui/src/org/netbeans/modules/java/source/ui/AsyncJavaSymbolDescriptor.java @@ -19,59 +19,37 @@ package org.netbeans.modules.java.source.ui; -import com.sun.tools.javac.api.ClientCodeWrapper; import com.sun.tools.javac.api.JavacTaskImpl; -import com.sun.tools.javac.api.JavacTool; -import com.sun.tools.javac.code.Symtab; -import com.sun.tools.javac.jvm.ClassReader; -import com.sun.tools.javac.model.JavacElements; -import java.io.File; -import java.io.IOException; -import java.lang.ref.Reference; -import java.lang.ref.WeakReference; -import java.lang.reflect.Field; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Logger; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.swing.Icon; -import javax.tools.Diagnostic; -import javax.tools.DiagnosticListener; -import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; -import javax.tools.StandardLocation; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.annotations.common.NullAllowed; -import org.netbeans.api.java.queries.SourceLevelQuery; +import org.netbeans.api.java.source.ClasspathInfo; import org.netbeans.api.java.source.ElementHandle; +import org.netbeans.api.java.source.ui.ElementOpen; import org.netbeans.api.project.ProjectInformation; import org.netbeans.modules.java.source.ElementHandleAccessor; import org.netbeans.modules.java.source.ElementUtils; -import org.netbeans.modules.java.source.indexing.JavaIndex; -import org.netbeans.modules.java.source.parsing.CachingArchiveProvider; -import org.netbeans.modules.java.source.parsing.CachingFileManager; +import org.netbeans.modules.java.source.parsing.JavacParser; import org.netbeans.modules.java.source.usages.ClassIndexImpl; -import org.netbeans.spi.java.classpath.support.ClassPathSupport; import org.netbeans.spi.jumpto.support.AsyncDescriptor; import org.netbeans.spi.jumpto.support.DescriptorChangeEvent; import org.netbeans.spi.jumpto.support.DescriptorChangeListener; import org.netbeans.spi.jumpto.symbol.SymbolDescriptor; import org.openide.filesystems.FileObject; -import org.openide.util.BaseUtilities; import org.openide.util.Exceptions; import org.openide.util.Pair; import org.openide.util.Parameters; @@ -85,11 +63,6 @@ final class AsyncJavaSymbolDescriptor extends JavaSymbolDescriptorBase implement private static final RequestProcessor WORKER = new RequestProcessor(AsyncJavaSymbolDescriptor.class); private static final String INIT = ""; //NOI18N - private static final Logger LOG = Logger.getLogger(AsyncJavaSymbolDescriptor.class.getName()); - private static volatile boolean pkgROELogged = false; - private static volatile boolean clzROELogged = false; - - private static Reference javacRef; private final String ident; private final boolean caseSensitive; @@ -131,6 +104,19 @@ public String getSimpleName() { @Override public void open() { final Collection symbols = resolve(); + /* resolve() falls back to Collections.singleton(this) if it could + not enrich the descriptor (e.g. javac threw CompletionFailure + for some transient reason). Detect that case explicitly to + avoid recursing into our own open(); fall back to opening the + type by its ElementHandle via ElementOpen, which goes through + ClassIndex/SourceUtils. */ + if (symbols.size() == 1 && symbols.iterator().next() == this) { + final FileObject file = getFileObject(); + if (file != null) { + ElementOpen.open(ClasspathInfo.create(file), getOwner()); + } + return; + } if (!symbols.isEmpty()) { symbols.iterator().next().open(); } @@ -197,26 +183,40 @@ private void fireDescriptorChange(Collection replace @NonNull private Collection resolve() { + final List symbols = new ArrayList<>(); try { - final List symbols = new ArrayList<>(); - final JavacTaskImpl jt = getJavac(getRoot()); -// final JavaFileManager fm = jt.getContext().get(JavaFileManager.class); -// final ClassReader cr = ClassReader.instance(jt.getContext()); -// final Names names = Names.instance(jt.getContext()); -// final String binName = ElementHandleAccessor.getInstance().getJVMSignature(getOwner())[0]; -// final Name jcName = names.fromString(binName); -// final Symbol.ClassSymbol te = cr.enterClass((com.sun.tools.javac.util.Name) jcName); -// te.owner.completer = null; -// te.classfile = fm.getJavaFileForInput( -// StandardLocation.CLASS_PATH, -// FileObjects.convertFolder2Package(binName), -// JavaFileObject.Kind.CLASS); - final Symtab syms = Symtab.instance(jt.getContext()); - final Set pkgs = new HashSet<>(getPackages(syms).keySet()); - final Set clzs = new HashSet<>(getClasses(syms).keySet()); - jt.getElements().getTypeElement("java.lang.Object"); // Ensure proper javac initialization - final TypeElement te = ElementUtils.getTypeElementByBinaryName(jt, - ElementHandleAccessor.getInstance().getJVMSignature(getOwner())[0]); + final String binName = ElementHandleAccessor.getInstance().getJVMSignature(getOwner())[0]; + /* Build the JavacTask via JavacParser.createJavacTask + (which pre-registers NetBeans' javac context enhancers -- + most importantly NBClassReader, plus NBAttr, NBEnter, + NBMemberEnter, NBResolve, NBClassFinder, NBJavaCompiler, + NBClassWriter, etc.) instead of calling + JavacTool.create().getTask(...) directly. NetBeans .sig + files (the per-root cached form of indexed Java classes) + are written by NBClassWriter and use a relaxed/extended + class-file format that only NBClassReader knows how to + read; stock javac's ClassReader rejects them with + BadClassFile, which propagates as CompletionFailure. + ElementUtils.getTypeElementByBinaryName silently catches + the CompletionFailure and returns null, and resolve() + therefore returned an empty collection for every Java + type whose .sig had any NB-specific encoding -- the + actual cause of the long-standing Go to Symbol + disappearance bug. */ + final ClasspathInfo cpInfo = ClasspathInfo.create(getRoot()); + final JavacTaskImpl jt = JavacParser.createJavacTask( + cpInfo, + null, + null, + null, + null, + null, + null, + null, + Collections.emptyList()); + //Force JTImpl.prepareCompiler to get JTImpl into Context + jt.enter(); + final TypeElement te = ElementUtils.getTypeElementByBinaryName(jt, binName); if (te != null) { if (ident.equals(getSimpleName(te, null, caseSensitive))) { final String simpleName = te.getSimpleName().toString(); @@ -252,69 +252,31 @@ private Collection resolve() { } } } - getClasses(syms).keySet().retainAll(clzs); - getPackages(syms).keySet().retainAll(pkgs); - return symbols; - } catch (IOException e) { + } catch (RuntimeException e) { + /* Swallow so that unexpected javac failures (e.g. + CompletionFailure, which is a RuntimeException) fall through + to the "no enriched symbols" path below rather than + propagating into the WORKER thread and leaving the descriptor + in a half-initialized state. Previously this was a catch for + IOException, thrown by the old hand-rolled JavaFileManager + wiring that has now been deleted. */ Exceptions.printStackTrace(e); - return Collections.emptyList(); } - } - - @NonNull - private Map getPackages(final Symtab cr) { - Map res = Collections.emptyMap(); - try { - final Field fld = ClassReader.class.getDeclaredField("packages"); //NOI18N - fld.setAccessible(true); - final Map pkgs = (Map) fld.get(cr); - if (pkgs != null) { - res = pkgs; - } - } catch (ReflectiveOperationException e) { - if (!pkgROELogged) { - LOG.warning(e.getMessage()); - pkgROELogged = true; - } + /* The async path is meant to *enrich* a descriptor that the + (Lucene-backed) JavaSymbolProvider already produced -- not to + delete it. If javac couldn't load the TypeElement (te==null), + no enclosed element matched ident, or the lookup threw, the + entry is still a valid match from the index and must remain + in the list. Returning an empty collection here would cause + Models.MutableListModelImpl#descriptorChanged to remove the + source from the live model, producing the long-standing + "search results appear briefly then disappear" Go to Symbol + symptom. Fall back to keeping the AsyncJavaSymbolDescriptor + itself. */ + if (symbols.isEmpty()) { + return Collections.singleton(this); } - return res; - } - - @NonNull - private Map getClasses(final Symtab cr) { - Map res = Collections.emptyMap(); - try { - final Field fld = ClassReader.class.getDeclaredField("classes"); //NOI18N - fld.setAccessible(true); - Map clzs = (Map) fld.get(cr); - if (clzs != null) { - res = clzs; - } - } catch (ReflectiveOperationException e) { - if (!clzROELogged) { - LOG.warning(e.getMessage()); - clzROELogged = true; - } - } - return res; - } - - private static JavacTaskImpl getJavac(FileObject root) throws IOException { - JavacTaskImpl javac; - Reference ref = javacRef; - if (ref == null || (javac = ref.get()) == null) { - String sourceLevel = SourceLevelQuery.getSourceLevel(root); - javac = (JavacTaskImpl)JavacTool.create().getTask(null, - new RootChange(root), - new Listener(), - sourceLevel != null ? Arrays.asList("-source", sourceLevel) : Collections.emptySet(), //NOI18N - Collections.emptySet(), - Collections.emptySet()); - javacRef = new WeakReference<>(javac); - } - final JavaFileManager fm = javac.getContext().get(JavaFileManager.class); - ((RootChange)fm).setRoot(root); - return javac; + return symbols; } @NonNull @@ -332,104 +294,4 @@ private static String getSimpleName ( return result; } - @ClientCodeWrapper.Trusted - private static final class RootChange implements JavaFileManager { - - private FileObject currentRoot; - private JavaFileManager delegate; - - RootChange(@NonNull final FileObject root) throws IOException { - setRoot(root); - } - - void setRoot(@NonNull final FileObject root) throws IOException { - if (root != currentRoot) { - final File classes = JavaIndex.getClassFolder(root.toURL()); - final CachingFileManager fm = new CachingFileManager( - CachingArchiveProvider.getDefault(), - ClassPathSupport.createClassPath(BaseUtilities.toURI(classes).toURL()), - null, - false, - true); - this.delegate = fm; - this.currentRoot = root; - } - } - - @Override - public ClassLoader getClassLoader(Location location) { - return delegate.getClassLoader(location); - } - - @Override - public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException { - return delegate.list(location, packageName, kinds, recurse); - } - - @Override - public String inferBinaryName(Location location, JavaFileObject file) { - return delegate.inferBinaryName(location, file); - } - - @Override - public boolean isSameFile(javax.tools.FileObject a, javax.tools.FileObject b) { - return delegate.isSameFile(a, b); - } - - @Override - public boolean handleOption(String current, Iterator remaining) { - return delegate.handleOption(current, remaining); - } - - @Override - public boolean hasLocation(Location location) { - return location == StandardLocation.CLASS_PATH || location == StandardLocation.PLATFORM_CLASS_PATH; - } - - @Override - public JavaFileObject getJavaFileForInput(Location location, String className, JavaFileObject.Kind kind) throws IOException { - return delegate.getJavaFileForInput(location, className, kind); - } - - @Override - public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, javax.tools.FileObject sibling) throws IOException { - return delegate.getJavaFileForOutput(location, className, kind, sibling); - } - - @Override - public javax.tools.FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException { - return delegate.getFileForInput(location, packageName, relativeName); - } - - @Override - public javax.tools.FileObject getFileForOutput(Location location, String packageName, String relativeName, javax.tools.FileObject sibling) throws IOException { - return delegate.getFileForOutput(location, packageName, relativeName, sibling); - } - - @Override - public void flush() throws IOException { - delegate.flush(); - } - - @Override - public void close() throws IOException { - delegate.close(); - } - - @Override - public int isSupportedOption(String option) { - return delegate.isSupportedOption(option); - } - - @Override - public Iterable> listLocationsForModules(Location location) throws IOException { - return Collections.emptyList(); - } - } - - private static final class Listener implements DiagnosticListener { - @Override - public void report(Diagnostic diagnostic) { - } - } } diff --git a/java/java.sourceui/test/unit/src/org/netbeans/modules/java/source/ui/JavaSymbolProviderTest.java b/java/java.sourceui/test/unit/src/org/netbeans/modules/java/source/ui/JavaSymbolProviderTest.java new file mode 100644 index 000000000000..a9e9c4b27a49 --- /dev/null +++ b/java/java.sourceui/test/unit/src/org/netbeans/modules/java/source/ui/JavaSymbolProviderTest.java @@ -0,0 +1,339 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.java.source.ui; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.netbeans.api.java.classpath.ClassPath; +import org.netbeans.api.java.classpath.GlobalPathRegistry; +import org.netbeans.api.java.platform.JavaPlatformManager; +import org.netbeans.junit.MockServices; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.java.source.parsing.FileObjects; +import org.netbeans.modules.java.source.usages.IndexUtil; +import org.netbeans.modules.jumpto.symbol.SymbolProviderAccessor; +import org.netbeans.modules.parsing.api.indexing.IndexingManager; +import org.netbeans.spi.java.classpath.ClassPathFactory; +import org.netbeans.spi.java.classpath.ClassPathImplementation; +import org.netbeans.spi.java.classpath.ClassPathProvider; +import org.netbeans.spi.java.classpath.PathResourceImplementation; +import org.netbeans.spi.java.classpath.support.ClassPathSupport; +import org.netbeans.spi.jumpto.support.AsyncDescriptor; +import org.netbeans.spi.jumpto.support.DescriptorChangeEvent; +import org.netbeans.spi.jumpto.support.DescriptorChangeListener; +import org.netbeans.spi.jumpto.symbol.SymbolDescriptor; +import org.netbeans.spi.jumpto.symbol.SymbolProvider; +import org.netbeans.spi.jumpto.type.SearchType; +import org.openide.filesystems.FileLock; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; + +/* This test was written entirely by Claude Opus 4.6, prompted by Eirik Bakke (ebakke@ultorg.com). +Eirik confirmed that the test failed before the fix to the bug in AsyncJavaSymbolDescriptor, and +passed after the fix was applied. */ + +/** + * Regression test for the long-standing Go to Symbol bug where + * AsyncJavaSymbolDescriptor.resolve() silently returned empty for any + * Java type whose .sig file used a NetBeans-specific class-file + * encoding, causing Lucene-indexed matches to flash briefly in the Go + * to Symbol list and then be deleted by + * Models.MutableListModelImpl#descriptorChanged. Root cause was that + * the old hand-rolled javac construction in resolve() called + * JavacTool.create().getTask(...) directly, which does not pre-register + * NetBeans' javac context enhancers (NBClassReader, NBAttr, NBEnter, + * NBMemberEnter, NBResolve, NBClassFinder, NBJavaCompiler, + * NBClassWriter, etc.). NetBeans .sig files (the per-root cached form + * of indexed Java classes) are written by NBClassWriter and use a + * relaxed/extended class-file format that only NBClassReader knows how + * to read; stock javac's ClassReader rejects them with BadClassFile + * (CompletionFailure), and ElementUtils.getTypeElementByBinaryName + * silently returned null. + * + *

The test creates a single-class source root whose source is + * intentionally rich enough (a lambda, generics, a static-final String + * constant, and a separate annotation type) that the indexer's .sig + * output is empirically rejected by stock javac. The exact attribute + * that trips stock ClassReader for this particular class has not been + * pinned down -- a trivial empty class produces a .sig that vanilla + * javac reads without trouble, so the richer source is required to + * exercise the regression. The test indexes the source, asks + * JavaSymbolProvider to find the class by name, triggers + * AsyncJavaSymbolDescriptor's async resolve() path via getSymbolName(), + * and asserts that the resulting descriptor-change event carries a + * non-empty, enriched replacement (not the original + * AsyncJavaSymbolDescriptor instance). Verified to fail against the + * pre-fix code path and pass against the fix. + */ +public class JavaSymbolProviderTest extends NbTestCase { + + private static FileObject srcRoot; + private static ClassPath sourcePath; + private static ClassPath compilePath; + private static ClassPath bootPath; + private static MutableCp spiCp; + private static MutableCp spiSrc; + + public JavaSymbolProviderTest(String name) { + super(name); + } + + @Override + protected void setUp() throws Exception { + clearWorkDir(); + File cache = new File(getWorkDir(), "cache"); //NOI18N + cache.mkdirs(); + IndexUtil.setCacheFolder(cache); + File src = new File(getWorkDir(), "src"); //NOI18N + src.mkdirs(); + srcRoot = FileUtil.toFileObject(src); + spiSrc = new MutableCp(Collections.singletonList(ClassPathSupport.createResource(srcRoot.getURL()))); + sourcePath = ClassPathFactory.createClassPath(spiSrc); + spiCp = new MutableCp(); + compilePath = ClassPathFactory.createClassPath(spiCp); + bootPath = JavaPlatformManager.getDefault().getDefaultPlatform().getBootstrapLibraries(); + MockServices.setServices(ClassPathProviderImpl.class); + + /* The class is intentionally rich (a lambda, generics, a + static-final String constant, and a separate annotation + type) so that the .sig file the indexer writes is + empirically rejected by stock javac's ClassReader. A trivial + empty class produces a .sig that vanilla javac can read, and + so would not exercise the bug we are regressing against. + The exact attribute that trips stock ClassReader for this + class has not been pinned down -- it is enough to know that + the differential exists. */ + createJavaFile(srcRoot, "org.me.test", "FooBar", + "package org.me.test;\n" + + "import java.lang.annotation.ElementType;\n" + + "import java.lang.annotation.Retention;\n" + + "import java.lang.annotation.RetentionPolicy;\n" + + "import java.lang.annotation.Target;\n" + + "@Retention(RetentionPolicy.SOURCE)\n" + + "@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})\n" + + "@interface SourceMark {}\n" + + "@SourceMark\n" + + "public class FooBar {\n" + + " public static final String GREETING = \"hello\";\n" + + " @SourceMark public final java.util.function.Supplier SUPPLIER = () -> GREETING;\n" + + " @SourceMark public void doSomething() {}\n" + + "}\n"); + /* JavaSymbolProvider queries GlobalPathRegistry for SOURCE + roots; it does not take a ClasspathInfo the way + JavaTypeProvider does. Register our test source path there + so findRoots(SOURCE, ...) picks it up. */ + GlobalPathRegistry.getDefault().register(ClassPath.SOURCE, new ClassPath[] {sourcePath}); + GlobalPathRegistry.getDefault().register(ClassPath.BOOT, new ClassPath[] {bootPath}); + GlobalPathRegistry.getDefault().register(ClassPath.COMPILE, new ClassPath[] {compilePath}); + IndexingManager.getDefault().refreshIndexAndWait(srcRoot.getURL(), null, true); + } + + @Override + protected void tearDown() throws Exception { + GlobalPathRegistry.getDefault().unregister(ClassPath.SOURCE, new ClassPath[] {sourcePath}); + GlobalPathRegistry.getDefault().unregister(ClassPath.BOOT, new ClassPath[] {bootPath}); + GlobalPathRegistry.getDefault().unregister(ClassPath.COMPILE, new ClassPath[] {compilePath}); + MockServices.setServices(); + } + + /** + * Regression test: asks JavaSymbolProvider for "FooBar", lets the + * returned AsyncJavaSymbolDescriptor fire its async resolve(), and + * asserts the resulting replacement is non-empty and not the + * original async descriptor instance. Prior to the fix, resolve() + * silently returned an empty collection and this assertion failed. + */ + public void testAsyncDescriptorResolvesForIndexedType() throws Exception { + final JavaSymbolProvider provider = new JavaSymbolProvider(); + final List results = new ArrayList<>(); + final String[] message = new String[1]; + final SymbolProvider.Context ctx = SymbolProviderAccessor.DEFAULT + .createContext(null, "FooBar", SearchType.PREFIX); //NOI18N + final SymbolProvider.Result res = SymbolProviderAccessor.DEFAULT + .createResult(results, message, ctx, provider); + provider.computeSymbolNames(ctx, res); + assertFalse("JavaSymbolProvider returned no hits for FooBar; " + + "the index may not have been populated in setUp", results.isEmpty()); + + // Find the AsyncDescriptor corresponding to the class itself. + AsyncDescriptor async = null; + for (SymbolDescriptor d : results) { + if (d instanceof AsyncDescriptor && "FooBar".equals(d.getSimpleName())) { //NOI18N + @SuppressWarnings("unchecked") + final AsyncDescriptor cast = (AsyncDescriptor) d; + async = cast; + break; + } + } + assertNotNull("JavaSymbolProvider did not return an AsyncDescriptor " + + "for class FooBar; results=" + results, async); + final AsyncDescriptor original = async; + + // Subscribe to the descriptor-change event that initialize() -> + // resolve() -> fireDescriptorChange() will fire on the WORKER + // thread once we touch getSymbolName() / getIcon(). + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference> replacementRef = new AtomicReference<>(); + final DescriptorChangeListener listener = + new DescriptorChangeListener() { + @Override + public void descriptorChanged(DescriptorChangeEvent event) { + replacementRef.set(event.getReplacement()); + latch.countDown(); + } + }; + original.addDescriptorChangeListener(listener); + try { + // Triggers AsyncJavaSymbolDescriptor.initialize() which + // schedules resolve() on the WORKER RequestProcessor. + ((SymbolDescriptor) original).getSymbolName(); + assertTrue("Async resolve() did not complete within 30s.", + latch.await(30, TimeUnit.SECONDS)); + } finally { + original.removeDescriptorChangeListener(listener); + } + + final Collection replacement = replacementRef.get(); + assertNotNull("replacement was null", replacement); + + /* The actual regression assertion. + Before the fix: resolve()'s hand-rolled + JavacTool.create().getTask(...) call had no NB + context enhancers, so javac used the stock + ClassReader, which threw CompletionFailure on + the .sig file (NetBeans .sig files use a relaxed + class-file format that only NBClassReader can + read). ElementUtils.getTypeElementByBinaryName + silently caught that and returned null, so + resolve() returned an empty collection and the + assertion below tripped. + After the fix: resolve() goes through + JavacParser.createJavacTask(ClasspathInfo.create( + root), ...), which pre-registers NBClassReader + and the rest of the NB javac context, so the + .sig file loads correctly and resolve() returns a + ResolvedJavaSymbolDescriptor distinct from the + source AsyncJavaSymbolDescriptor. */ + assertFalse("resolve() returned an empty replacement -- the bug is " + + "still present: AsyncJavaSymbolDescriptor.resolve() is " + + "silently failing to enrich the Lucene-indexed hit. Most " + + "likely cause: the JavacTask was built without " + + "pre-registering NBClassReader, so stock javac could not " + + "parse the per-root .sig file.", + replacement.isEmpty()); + for (SymbolDescriptor d : replacement) { + assertNotSame("resolve() fell back to its singleton(this) " + + "defense-in-depth path instead of actually enriching " + + "the descriptor. This still means javac could not " + + "complete() the type; most likely the JavacTask was " + + "not built with NB context enhancers (NBClassReader, " + + "etc.) and so cannot read NetBeans .sig files.", + original, d); + } + } + + private static FileObject createJavaFile( + final FileObject root, + final String pkg, + final String name, + final String content) throws IOException { + final FileObject file = FileUtil.createData( + root, + String.format("%s/%s.java", + FileObjects.convertPackage2Folder(pkg), + name)); + final FileLock lck = file.lock(); + try { + final PrintWriter out = new PrintWriter(new OutputStreamWriter(file.getOutputStream(lck))); + try { + out.print(content); + } finally { + out.close(); + } + } finally { + lck.releaseLock(); + } + return file; + } + + public static class ClassPathProviderImpl implements ClassPathProvider { + @Override + public ClassPath findClassPath(final FileObject file, final String type) { + final FileObject[] roots = sourcePath.getRoots(); + for (FileObject root : roots) { + if (root.equals(file) || FileUtil.isParentOf(root, file)) { + if (type == ClassPath.SOURCE) { + return sourcePath; + } + if (type == ClassPath.COMPILE) { + return compilePath; + } + if (type == ClassPath.BOOT) { + return bootPath; + } + } + } + return null; + } + } + + private static final class MutableCp implements ClassPathImplementation { + + private final PropertyChangeSupport support; + private List impls; + + MutableCp() { + this(Collections.emptyList()); + } + + MutableCp(final List impls) { + assert impls != null; + support = new PropertyChangeSupport(this); + this.impls = impls; + } + + @Override + public List getResources() { + return impls; + } + + @Override + public void addPropertyChangeListener(final PropertyChangeListener listener) { + assert listener != null; + support.addPropertyChangeListener(listener); + } + + @Override + public void removePropertyChangeListener(final PropertyChangeListener listener) { + assert listener != null; + support.removePropertyChangeListener(listener); + } + } +}