blob: 17e17caffdad5ee286076d433c64184ca94f1a08 [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.java.hints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.Types;
import javax.swing.text.JTextComponent;
import javax.tools.Diagnostic;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import org.netbeans.api.java.source.support.ErrorAwareTreePathScanner;
import com.sun.source.util.Trees;
import com.sun.tools.javac.code.Scope;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.util.Name;
import org.netbeans.api.editor.EditorActionNames;
import org.netbeans.api.editor.EditorActionRegistration;
import org.netbeans.api.java.source.CodeStyle;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.GeneratorUtilities;
import org.netbeans.api.java.source.JavaSource.Phase;
import org.netbeans.api.java.source.ModificationResult;
import org.netbeans.api.java.source.ModificationResult.Difference;
import org.netbeans.api.java.source.TreeMaker;
import org.netbeans.api.java.source.WorkingCopy;
import org.netbeans.api.progress.ProgressUtils;
import org.netbeans.editor.BaseAction;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.editor.java.JavaKit;
import org.netbeans.modules.java.editor.base.javadoc.JavadocImports;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.UserTask;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.spi.editor.hints.ErrorDescription;
import org.netbeans.spi.editor.hints.Fix;
import org.netbeans.spi.java.hints.ErrorDescriptionFactory;
import org.netbeans.spi.java.hints.Hint;
import org.netbeans.spi.java.hints.HintContext;
import org.netbeans.spi.java.hints.JavaFix;
import org.netbeans.spi.java.hints.TriggerTreeKind;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
/**
*
* @author Dusan Balek
*/
@Hint(displayName = "#DN_org.netbeans.modules.java.hints.OrganizeImports", description = "#DESC_org.netbeans.modules.java.hints.OrganizeImports", category = "imports", enabled = false)
public class OrganizeImports {
private static final String ERROR_CODE = "compiler.err.expected"; // NOI18N
@TriggerTreeKind(Kind.COMPILATION_UNIT)
public static ErrorDescription checkImports(final HintContext context) {
Source source = context.getInfo().getSnapshot().getSource();
ModificationResult result = null;
try {
result = ModificationResult.runModificationTask(Collections.singleton(source), new UserTask() {
@Override
public void run(ResultIterator resultIterator) throws Exception {
WorkingCopy copy = WorkingCopy.get(resultIterator.getParserResult());
copy.toPhase(Phase.RESOLVED);
doOrganizeImports(copy, context.isBulkMode());
}
});
} catch (ParseException ex) {
Exceptions.printStackTrace(ex);
}
List<? extends Difference> diffs = result != null ? result.getDifferences(source.getFileObject()) : null;
if (diffs != null && !diffs.isEmpty()) {
Fix fix = new OrganizeImportsFix(context.getInfo(), context.getPath(), context.isBulkMode()).toEditorFix();
SourcePositions sp = context.getInfo().getTrees().getSourcePositions();
int offset = diffs.get(0).getStartPosition().getOffset();
CompilationUnitTree cu = context.getInfo().getCompilationUnit();
for (ImportTree imp : cu.getImports()) {
if (sp.getEndPosition(cu, imp) >= offset)
return ErrorDescriptionFactory.forTree(context, imp, NbBundle.getMessage(OrganizeImports.class, "MSG_OragnizeImports"), fix); //NOI18N
}
return ErrorDescriptionFactory.forTree(context, context.getInfo().getCompilationUnit().getImports().get(0), NbBundle.getMessage(OrganizeImports.class, "MSG_OragnizeImports"), fix); //NOI18N
}
return null;
}
private static void doOrganizeImports(WorkingCopy copy, boolean isBulkMode) throws IllegalStateException {
doOrganizeImports(copy, null, isBulkMode);
}
/**
* Performs 'organize imports' in two modes. If 'addImports' is null, it optimizes imports according to coding style.
* However if addImports is not null && not empty, it will add the elements in addImports, and reorder the set
* of import statements, but will not remove unused imports.
* Combination of bulk + addImports is not tested.
*
* @param copy working copy to change
* @param addImports if not null, just adds and reorders imports. If null, performs optimization
* @param isBulkMode called from batch processing
* @throws IllegalStateException
*/
public static void doOrganizeImports(WorkingCopy copy, Set<Element> addImports, boolean isBulkMode) throws IllegalStateException {
CompilationUnitTree cu = copy.getCompilationUnit();
List<? extends ImportTree> imports = cu.getImports();
if (imports.isEmpty()) {
if (addImports == null) {
return;
}
} else if (addImports == null) {
// check diag code only if in the 'optimize all' mode.
List<Diagnostic> diags = copy.getDiagnostics();
if (!diags.isEmpty()) {
SourcePositions sp = copy.getTrees().getSourcePositions();
long startPos = sp.getStartPosition(cu, imports.get(0));
long endPos = sp.getEndPosition(cu, imports.get(imports.size() - 1));
for (Diagnostic d : diags) {
if (startPos <= d.getPosition() && d.getPosition() <= endPos) {
if (ERROR_CODE.contentEquals(d.getCode()))
return;
}
}
}
}
final CodeStyle cs = CodeStyle.getDefault(copy.getFileObject());
Set<Element> starImports = new HashSet<Element>();
Set<Element> staticStarImports = new HashSet<Element>();
Set<Element> toImport = getUsedElements(copy, cu, starImports, staticStarImports);
List<ImportTree> imps = new LinkedList<ImportTree>();
TreeMaker maker = copy.getTreeMaker();
if (addImports != null) {
// copy over all imports to be added + existing imports.
toImport.addAll(addImports);
imps.addAll(cu.getImports());
} else if (!toImport.isEmpty() || isBulkMode) {
// track import star import scopes, so only one star import/scope appears in imps - #251977
Set<Element> starImportScopes = new HashSet<>();
Trees trees = copy.getTrees();
for (ImportTree importTree : cu.getImports()) {
Tree qualIdent = importTree.getQualifiedIdentifier();
if (qualIdent.getKind() == Tree.Kind.MEMBER_SELECT && "*".contentEquals(((MemberSelectTree)qualIdent).getIdentifier())) {
ImportTree imp = null;
Element importedScope = trees.getElement(TreePath.getPath(cu, ((MemberSelectTree)qualIdent).getExpression()));
if (importTree.isStatic()) {
if (staticStarImports != null &&
staticStarImports.contains(importedScope) &&
!starImportScopes.contains(importedScope)) {
imp = maker.Import(qualIdent, true);
}
} else {
if (starImports != null &&
starImports.contains(importedScope) &&
!starImportScopes.contains(importedScope)) {
imp = maker.Import(qualIdent, false);
}
}
if (imp != null) {
starImportScopes.add(importedScope);
imps.add(imp);
}
}
}
} else {
return;
}
if (!imps.isEmpty()) {
Collections.sort(imps, new Comparator<ImportTree>() {
private CodeStyle.ImportGroups groups = cs.getImportGroups();
@Override
public int compare(ImportTree o1, ImportTree o2) {
if (o1 == o2)
return 0;
String s1 = o1.getQualifiedIdentifier().toString();
String s2 = o2.getQualifiedIdentifier().toString();
int bal = groups.getGroupId(s1, o1.isStatic()) - groups.getGroupId(s2, o2.isStatic());
return bal == 0 ? s1.compareTo(s2) : bal;
}
});
}
CompilationUnitTree cut = maker.CompilationUnit(cu.getPackageAnnotations(), cu.getPackageName(), imps, cu.getTypeDecls(), cu.getSourceFile());
((JCCompilationUnit)cut).packge = ((JCCompilationUnit)cu).packge;
if (starImports != null || staticStarImports != null) {
((JCCompilationUnit)cut).starImportScope = ((JCCompilationUnit)cu).starImportScope;
}
CompilationUnitTree ncu = toImport.isEmpty() ? cut : GeneratorUtilities.get(copy).addImports(cut, toImport);
copy.rewrite(cu, ncu);
}
private static Set<Element> getUsedElements(final CompilationInfo info, final CompilationUnitTree cut, final Set<Element> starImports, final Set<Element> staticStarImports) {
final Set<Element> ret = new HashSet<Element>();
final Trees trees = info.getTrees();
final Types types = info.getTypes();
new ErrorAwareTreePathScanner<Void, Void>() {
@Override
public Void visitIdentifier(IdentifierTree node, Void p) {
addElement(trees.getElement(getCurrentPath()));
return null;
}
@Override
public Void visitClass(ClassTree node, Void p) {
for (Element element : JavadocImports.computeReferencedElements(info, getCurrentPath()))
addElement(element);
return super.visitClass(node, p);
}
@Override
public Void visitMethod(MethodTree node, Void p) {
for (Element element : JavadocImports.computeReferencedElements(info, getCurrentPath()))
addElement(element);
return super.visitMethod(node, p);
}
@Override
public Void visitVariable(VariableTree node, Void p) {
for (Element element : JavadocImports.computeReferencedElements(info, getCurrentPath()))
addElement(element);
return super.visitVariable(node, p);
}
@Override
public Void visitCompilationUnit(CompilationUnitTree node, Void p) {
scan(node.getPackageAnnotations(), p);
return scan(node.getTypeDecls(), p);
}
private void addElement(Element element) {
if (element != null) {
switch (element.getKind()) {
case ENUM_CONSTANT:
case FIELD:
case METHOD:
if (element.getModifiers().contains(Modifier.STATIC)) {
Element glob = global(element, staticStarImports);
if (glob != null)
ret.add(glob);
}
break;
case ANNOTATION_TYPE:
case CLASS:
case ENUM:
case INTERFACE:
Element glob = global(element, starImports);
if (glob != null)
ret.add(glob);
}
}
}
private Element global(Element element, Set<Element> stars) {
for (Symbol sym : ((JCCompilationUnit)cut).namedImportScope.getSymbolsByName((Name)element.getSimpleName())) {
if (element == sym || element.asType().getKind() == TypeKind.ERROR && element.getKind() == sym.getKind())
return sym;
}
for (Symbol sym : ((JCCompilationUnit)cut).packge.members().getSymbolsByName((Name)element.getSimpleName())) {
if (element == sym || element.asType().getKind() == TypeKind.ERROR && element.getKind() == sym.getKind())
return sym;
}
for (Symbol sym : ((JCCompilationUnit)cut).starImportScope.getSymbolsByName((Name)element.getSimpleName())) {
if (element == sym || element.asType().getKind() == TypeKind.ERROR && element.getKind() == sym.getKind()) {
if (stars != null) {
stars.add(sym.owner);
}
return sym;
}
}
return null;
}
}.scan(cut, null);
return ret;
}
private static final class OrganizeImportsFix extends JavaFix {
private final boolean isBulkMode;
public OrganizeImportsFix(CompilationInfo info, TreePath tp, boolean isBulkMode) {
super(info, tp);
this.isBulkMode = isBulkMode;
}
@Override
public String getText() {
return NbBundle.getMessage(OrganizeImports.class, "FIX_OrganizeImports"); //NOI18N
}
@Override
protected void performRewrite(TransformationContext ctx) {
doOrganizeImports(ctx.getWorkingCopy(), isBulkMode);
}
}
@EditorActionRegistration(name = EditorActionNames.organizeImports,
mimeType = JavaKit.JAVA_MIME_TYPE,
menuPath = "Source",
menuPosition = 2430,
menuText = "#" + EditorActionNames.organizeImports + "_menu_text")
public static class OrganizeImportsAction extends BaseAction {
@Override
public void actionPerformed(final ActionEvent evt, final JTextComponent component) {
if (component == null || !component.isEditable() || !component.isEnabled()) {
Toolkit.getDefaultToolkit().beep();
return;
}
final BaseDocument doc = (BaseDocument) component.getDocument();
final Source source = Source.create(doc);
if (source != null) {
final AtomicBoolean cancel = new AtomicBoolean();
ProgressUtils.runOffEventDispatchThread(new Runnable() {
@Override
public void run() {
try {
ModificationResult.runModificationTask(Collections.singleton(source), new UserTask() {
@Override
public void run(ResultIterator resultIterator) throws Exception {
WorkingCopy copy = WorkingCopy.get(resultIterator.getParserResult());
copy.toPhase(Phase.RESOLVED);
doOrganizeImports(copy, false);
}
}).commit();
} catch (Exception ex) {
Toolkit.getDefaultToolkit().beep();
}
}
}, NbBundle.getMessage(OrganizeImports.class, "MSG_OragnizeImports"), cancel, false); //NOI18N
}
}
}
}