blob: 71e38c9b134ba82e0b0b73ff9f08e0e55c372c07 [file] [log] [blame]
/**
* 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.jackpot30.cmdline.processor;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.AbstractPreferences;
import java.util.prefs.BackingStoreException;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileManager;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.source.ClasspathInfo;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.Task;
import org.netbeans.modules.editor.tools.storage.api.ToolPreferences;
import org.netbeans.modules.jackpot30.cmdline.lib.Utils;
import org.netbeans.modules.java.hints.providers.spi.HintDescription;
import org.netbeans.modules.java.hints.providers.spi.HintMetadata;
import org.netbeans.modules.java.hints.spiimpl.RulesManager;
import org.netbeans.modules.java.hints.spiimpl.hints.HintsInvoker;
import org.netbeans.modules.java.hints.spiimpl.options.HintsSettings;
import org.netbeans.modules.parsing.impl.indexing.CacheFolder;
import org.netbeans.spi.editor.hints.ErrorDescription;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
import org.netbeans.spi.java.hints.Hint;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.URLMapper;
import org.openide.util.Lookup;
/**
*
* @author lahvac
*/
@SupportedAnnotationTypes("*")
@SupportedOptions({"hintsConfiguration", "disableJackpotProcessor"})
public class ProcessorImpl extends AbstractProcessor {
public static final String CONFIGURATION_OPTION = "hintsConfiguration";
private final Map<URL, CompilationUnitTree> sources = new HashMap<>();
private static final Logger TOP_LOGGER = Logger.getLogger("");
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if ("true".equals(processingEnv.getOptions().get("disableJackpotProcessor")))
return false;
if (!roundEnv.processingOver()) {
Trees trees = Trees.instance(processingEnv);
for (Element root : roundEnv.getRootElements()) {
TypeElement outtermost = outtermostType(root);
TreePath path = trees.getPath(outtermost);
if (path == null) {
//TODO: log
continue;
}
try {
sources.put(path.getCompilationUnit().getSourceFile().toUri().toURL(), path.getCompilationUnit());
} catch (MalformedURLException ex) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Unexpected exception: " + ex.getMessage());
Logger.getLogger(ProcessorImpl.class.getName()).log(Level.SEVERE, null, ex);
}
}
} else {
JavacTask.instance(processingEnv).addTaskListener(new TaskListener() {
@Override
public void started(TaskEvent e) {}
@Override
public void finished(TaskEvent evt) {
if (evt.getKind() == TaskEvent.Kind.ENTER) {
runHints();
}
}
});
}
return false;
}
private void runHints() {
Utils.addExports();
Trees trees = Trees.instance(processingEnv);
Level originalLoggerLevel = TOP_LOGGER.getLevel();
Path toDelete = null;
try {
TOP_LOGGER.setLevel(Level.OFF);
System.setProperty("RepositoryUpdate.increasedLogLevel", "OFF");
Method getContext = processingEnv.getClass().getDeclaredMethod("getContext");
Object context = getContext.invoke(processingEnv);
Method get = context.getClass().getDeclaredMethod("get", Class.class);
JavaFileManager fileManager = (JavaFileManager) get.invoke(context, JavaFileManager.class);
if (!(fileManager instanceof StandardJavaFileManager)) {
processingEnv.getMessager().printMessage(Kind.ERROR, "The file manager is not a StandardJavaFileManager, cannot run Jackpot 3.0.");
return ;
}
setupCache();
StandardJavaFileManager sfm = (StandardJavaFileManager) fileManager;
ClassPath bootCP = /*XXX*/Utils.createDefaultBootClassPath();//toClassPath(sfm.getLocation(StandardLocation.PLATFORM_CLASS_PATH));
ClassPath compileCP = toClassPath(sfm.getLocation(StandardLocation.CLASS_PATH));
Iterable<? extends File> sourcePathLocation = sfm.getLocation(StandardLocation.SOURCE_PATH);
ClassPath sourceCP = sourcePathLocation != null ? toClassPath(sourcePathLocation) : inferSourcePath();
final Map<FileObject, CompilationUnitTree> sourceFiles = new HashMap<>();
for (Entry<URL, CompilationUnitTree> e : sources.entrySet()) {
FileObject fo = URLMapper.findFileObject(e.getKey());
if (fo == null) {
//XXX:
return ;
}
sourceFiles.put(fo, e.getValue());
}
URI settingsURI;
String configurationFileLoc = processingEnv.getOptions().get(CONFIGURATION_OPTION);
File configurationFile = configurationFileLoc != null ? new File(configurationFileLoc) : null;
if (configurationFile == null || !configurationFile.canRead()) {
URL cfg = ProcessorImpl.class.getResource("/org/netbeans/modules/jackpot30/cmdline/processor/cfg_hints.xml");
Path tmp = Files.createTempFile("cfg_hints", "xml"); //TODO: delete
try (InputStream cfgIn = cfg.openStream();
OutputStream out = Files.newOutputStream(tmp)) {
int read;
while ((read = cfgIn.read()) != (-1))
out.write(read);
}
settingsURI = tmp.toUri();
toDelete = tmp;
} else {
settingsURI = configurationFile.toURI();
}
HintsSettings settings = HintsSettings.createPreferencesBasedHintsSettings(ToolPreferences.from(settingsURI).getPreferences("hints", "text/x-java"), true, null);
final Map<HintMetadata, ? extends Collection<? extends HintDescription>> allHints;
java.io.PrintStream oldErr = System.err;
try {
//XXX: TreeUtilities.unenter prints exceptions to stderr on JDK 11, throw the output away:
System.setErr(new java.io.PrintStream(new java.io.ByteArrayOutputStream()));
allHints = RulesManager.getInstance().readHints(null, Arrays.asList(bootCP, compileCP, sourceCP), new AtomicBoolean());
} finally {
System.setErr(oldErr);
}
List<HintDescription> hints = new ArrayList<>();
for (Entry<HintMetadata, ? extends Collection<? extends HintDescription>> e : allHints.entrySet()) {
if (settings.isEnabled(e.getKey()) && e.getKey().kind == Hint.Kind.INSPECTION && !e.getKey().options.contains(HintMetadata.Options.NO_BATCH)) {
hints.addAll(e.getValue());
}
}
final Map<String, String> id2DisplayName = Utils.computeId2DisplayName(hints);
ClasspathInfo cpInfo = new ClasspathInfo.Builder(bootCP).setClassPath(compileCP).setSourcePath(sourceCP).setModuleBootPath(bootCP).build();
JavaSource.create(cpInfo, sourceFiles.keySet()).runUserActionTask(new Task<CompilationController>() {
@Override
public void run(CompilationController parameter) throws Exception {
if (parameter.toPhase(JavaSource.Phase.RESOLVED).compareTo(JavaSource.Phase.RESOLVED) < 0) {
return;
}
List<ErrorDescription> eds = new HintsInvoker(settings, /*XXX*/new AtomicBoolean()).computeHints(parameter, hints);
if (eds != null) {
//TODO: sort errors!!!
for (ErrorDescription ed : eds) {
CompilationUnitTree originalUnit = sourceFiles.get(ed.getFile());
if (originalUnit == null) {
//XXX: log properly!!!
continue;
}
TreePath posPath = pathFor(originalUnit, trees.getSourcePositions(), ed.getRange().getBegin().getOffset());
String category = Utils.categoryName(ed.getId(), id2DisplayName);
Kind diagKind;
switch (ed.getSeverity()) {
case ERROR: diagKind = Kind.ERROR; break;
case VERIFIER:
case WARNING: diagKind = Kind.WARNING; break;
case HINT:
default: diagKind = Kind.NOTE; break;
}
trees.printMessage(diagKind, category + ed.getDescription(), posPath.getLeaf(), posPath.getCompilationUnit());
}
}
}
}, true);
} catch (SecurityException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException | InvocationTargetException | IOException ex) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Unexpected exception: " + ex.getMessage());
Logger.getLogger(ProcessorImpl.class.getName()).log(Level.SEVERE, null, ex);
} finally {
TOP_LOGGER.setLevel(originalLoggerLevel);
if (toDelete != null) {
try {
Files.delete(toDelete);
} catch (IOException ex) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Unexpected exception: " + ex.getMessage());
Logger.getLogger(ProcessorImpl.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
private static ClassPath toClassPath(Iterable<? extends File> files) throws MalformedURLException {
Collection<URL> roots = new ArrayList<>();
if (files != null) {
for (File f : files) {
roots.add(FileUtil.urlForArchiveOrDir(FileUtil.normalizeFile(f)));
}
}
return ClassPathSupport.createClassPath(roots.toArray(new URL[0]));
}
private void setupCache() throws IOException {
File tmp = File.createTempFile("jackpot30", null);
tmp.delete();
tmp.mkdirs();
tmp.deleteOnExit();
tmp = FileUtil.normalizeFile(tmp);
FileUtil.refreshFor(tmp.getParentFile());
org.openide.filesystems.FileObject tmpFO = FileUtil.toFileObject(tmp);
if (tmpFO != null) {
CacheFolder.setCacheFolder(tmpFO);
}
}
private TypeElement outtermostType(Element el) {
while (/*XXX: package/module-info!*/el.getEnclosingElement() != null && el.getEnclosingElement().getKind() != ElementKind.PACKAGE) {
el = el.getEnclosingElement();
}
if (el.getKind() == ElementKind.PACKAGE || el.getKind().name().equals("MODULE"))
return null;
return (TypeElement) el;
}
private TreePath pathFor(final CompilationUnitTree cut, final SourcePositions sp, final long pos) {
class Result extends RuntimeException {
final TreePath result;
public Result(TreePath result) {
this.result = result;
}
}
try {
new TreePathScanner<Void, Void>() {
@Override
public Void scan(Tree tree, Void p) {
long s = sp.getStartPosition(cut, tree);
long e = sp.getEndPosition(cut, tree);
if (s <= pos && pos <= e) {
super.scan(tree, p);
throw new Result(getCurrentPath());
}
return null;
}
}.scan(cut, null);
} catch (Result r) {
return r.result;
}
return null;
}
private ClassPath inferSourcePath() {
if (sources.isEmpty())
return ClassPath.EMPTY;
Entry<URL, CompilationUnitTree> e = sources.entrySet().iterator().next();
FileObject sourceRoot = URLMapper.findFileObject(e.getKey());
if (sourceRoot == null) {
//unexpected
return ClassPath.EMPTY;
}
sourceRoot = sourceRoot.getParent();
for (String part : e.getValue().getPackageName().toString().split("\\.")) {
sourceRoot = sourceRoot.getParent();
}
return ClassPathSupport.createClassPath(sourceRoot);
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
static {
ClassLoader origContext = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(ProcessorImpl.class.getClassLoader());
Lookup.getDefault();
} finally {
Thread.currentThread().setContextClassLoader(origContext);
}
}
private static final class ModuleAndClass {
public final String module;
public final String name;
public ModuleAndClass(String module, String name) {
this.module = module;
this.name = name;
}
}
private static final class DummyPreferences extends AbstractPreferences {
private final Map<String, String> values = new HashMap<>();
private final Map<String, DummyPreferences> subNodes = new HashMap<>();
public DummyPreferences(AbstractPreferences parent, String name) {
super(parent, name);
}
@Override
protected void putSpi(String key, String value) {
values.put(key, value);
}
@Override
protected String getSpi(String key) {
return values.get(key);
}
@Override
protected void removeSpi(String key) {
values.remove(key);
}
@Override
protected void removeNodeSpi() throws BackingStoreException {
((DummyPreferences) parent()).subNodes.remove(name());
}
@Override
protected String[] keysSpi() throws BackingStoreException {
return values.keySet().toArray(new String[0]);
}
@Override
protected String[] childrenNamesSpi() throws BackingStoreException {
return subNodes.keySet().toArray(new String[0]);
}
@Override
protected AbstractPreferences childSpi(String name) {
DummyPreferences n = subNodes.get(name);
if (n == null) {
subNodes.put(name, n = new DummyPreferences(this, name));
}
return n;
}
@Override
protected void syncSpi() throws BackingStoreException {
}
@Override
protected void flushSpi() throws BackingStoreException {
}
}
}