blob: 4bacc97c8c118e19bdcb23208c2bdd628ed12914 [file] [log] [blame]
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2015 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*
* Contributor(s):
*
* Portions Copyrighted 2015 Sun Microsystems, Inc.
*/
package org.netbeans.modules.jackpot30.ap;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.SourcePositions;
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.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
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 java.util.prefs.Preferences;
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.editor.hints.Severity;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
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")
public class ProcessorImpl extends AbstractProcessor {
public static final String CONFIGURATION_OPTION = "hintsConfiguration";
private final Map<URL, String> sources = new HashMap<>();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
final Trees trees = Trees.instance(processingEnv);
if (!roundEnv.processingOver()) {
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(), outtermost.getQualifiedName().toString());
} catch (MalformedURLException ex) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Unexpected exception: " + ex.getMessage());
Logger.getLogger(ProcessorImpl.class.getName()).log(Level.SEVERE, null, ex);
}
}
} else {
try {
Field contextField = processingEnv.getClass().getDeclaredField("context");
contextField.setAccessible(true);
Object context = contextField.get(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 false;
}
setupCache();
StandardJavaFileManager sfm = (StandardJavaFileManager) fileManager;
ClassPath bootCP = 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, String> sourceFiles = new HashMap<>();
for (Entry<URL, String> e : sources.entrySet()) {
FileObject fo = URLMapper.findFileObject(e.getKey());
if (fo == null) {
//XXX:
return false;
}
sourceFiles.put(fo, e.getValue());
}
final Map<HintMetadata, ? extends Collection<? extends HintDescription>> allHints = RulesManager.getInstance().readHints(null, Arrays.asList(bootCP, compileCP, sourceCP), new AtomicBoolean());
List<HintDescription> hints = new ArrayList<>();
for (Collection<? extends HintDescription> v : allHints.values()) {
hints.addAll(v);
}
final Map<String, String> id2DisplayName = Utils.computeId2DisplayName(hints);
final Map<HintMetadata, ? extends Collection<? extends HintDescription>> hardcodedHints = RulesManager.getInstance().readHints(null, null, new AtomicBoolean());
final HintsSettings settings;
String configurationFileLoc = processingEnv.getOptions().get(CONFIGURATION_OPTION);
File configurationFile = configurationFileLoc != null ? new File(configurationFileLoc) : null;
if (configurationFile == null || !configurationFile.canRead()) {
settings = new HintsSettings() {
@Override
public boolean isEnabled(HintMetadata hm) {
return !hardcodedHints.containsKey(hm) ? hm.enabled : false;
}
@Override
public void setEnabled(HintMetadata hm, boolean bln) {
throw new UnsupportedOperationException();
}
@Override
public Preferences getHintPreferences(HintMetadata hm) {
return new DummyPreferences(null, "");
}
@Override
public Severity getSeverity(HintMetadata hm) {
return hm.severity;
}
@Override
public void setSeverity(HintMetadata hm, Severity svrt) {
throw new UnsupportedOperationException();
}
};
} else {
settings = HintsSettings.createPreferencesBasedHintsSettings(ToolPreferences.from(configurationFile.toURI()).getPreferences("hints", "text/x-java"), true, null);
}
ClasspathInfo cpInfo = ClasspathInfo.create(bootCP, compileCP, sourceCP);
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);
if (eds != null) {
for (ErrorDescription ed : eds) {
String outtermost = sourceFiles.get(ed.getFile());
TypeElement type = processingEnv.getElementUtils().getTypeElement(outtermost); //XXX: package-info!!!
TreePath typePath = trees.getPath(type);
TreePath posPath = pathFor(typePath.getCompilationUnit(), 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 (NoSuchFieldException | 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);
}
}
return false;
}
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 (el.getEnclosingElement().getKind() != ElementKind.PACKAGE) { //XXX: package-info!
el = el.getEnclosingElement();
}
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, String> e = sources.entrySet().iterator().next();
FileObject sourceRoot = URLMapper.findFileObject(e.getKey());
if (sourceRoot == null) {
//unexpected
return ClassPath.EMPTY;
}
for (String part : e.getValue().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 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 {
}
}
}