blob: 2b7bb50c31e83f360b396708cd8092e82a49918e [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.editor.base.semantic;
import com.sun.source.tree.BreakTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ContinueTree;
import com.sun.source.tree.DoWhileLoopTree;
import com.sun.source.tree.EnhancedForLoopTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ForLoopTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.LabeledStatementTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.TryTree;
import com.sun.source.tree.WhileLoopTree;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import org.netbeans.api.java.source.support.ErrorAwareTreePathScanner;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.ElementFilter;
import javax.swing.text.Document;
import org.netbeans.api.java.lexer.JavaTokenId;
import org.netbeans.api.java.lexer.JavadocTokenId;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.JavaParserResultTask;
import org.netbeans.api.java.source.JavaSource.Phase;
import org.netbeans.api.java.source.TreeUtilities;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.java.editor.base.javadoc.JavadocImports;
import org.netbeans.modules.java.editor.base.options.MarkOccurencesSettingsNames;
import org.netbeans.modules.parsing.spi.Parser.Result;
import org.netbeans.modules.parsing.spi.Scheduler;
import org.netbeans.modules.parsing.spi.SchedulerEvent;
import org.netbeans.modules.parsing.spi.TaskIndexingMode;
/**
*
* @author Jan Lahoda
*/
public abstract class MarkOccurrencesHighlighterBase extends JavaParserResultTask {
protected MarkOccurrencesHighlighterBase() {
super(Phase.RESOLVED, TaskIndexingMode.ALLOWED_DURING_SCAN);
}
@Override
public void run (Result parseResult, SchedulerEvent event) {
resume();
CompilationInfo info = CompilationInfo.get(parseResult);
if (info == null) {
return ;
}
Document doc = parseResult.getSnapshot().getSource().getDocument(false);
process(info, doc, event);
}
protected abstract void process(CompilationInfo info, Document doc, SchedulerEvent event);
private boolean isIn(CompilationUnitTree cu, SourcePositions sp, Tree tree, int position) {
return sp.getStartPosition(cu, tree) <= position && position <= sp.getEndPosition(cu, tree);
}
private boolean isIn(int caretPosition, Token span) {
// System.err.println("caretPosition = " + caretPosition );
// System.err.println("span[0]= " + span[0]);
// System.err.println("span[1]= " + span[1]);
if (span == null)
return false;
return span.offset(null) <= caretPosition && caretPosition <= span.offset(null) + span.length();
}
protected List<int[]> processImpl(CompilationInfo info, Preferences node, Document doc, int caretPosition) {
caretPosition = info.getSnapshot().getEmbeddedOffset(caretPosition);
TokenSequence<JavaTokenId> cts = info.getTokenHierarchy().tokenSequence(JavaTokenId.language());
if (cts != null) {
cts.move(caretPosition);
if (cts.moveNext() && cts.token().id() == JavaTokenId.IDENTIFIER && cts.offset() == caretPosition) {
caretPosition++;
}
}
CompilationUnitTree cu = info.getCompilationUnit();
TreeUtilities tu = info.getTreeUtilities();
TreePath tp = tu.pathFor(caretPosition);
if (tp.getParentPath() != null && tp.getParentPath().getLeaf().getKind() == Kind.ANNOTATED_TYPE) {
tp = tp.getParentPath();
}
TreePath typePath = findTypePath(tp);
if (isCancelled())
return null;
//detect caret inside the return type or throws clause:
if (typePath != null && typePath.getParentPath().getLeaf().getKind() == Kind.METHOD) {
//hopefully found something, check:
TreePath declPath = typePath.getParentPath();
MethodTree decl = (MethodTree) declPath.getLeaf();
Tree type = decl.getReturnType();
if ( node.getBoolean(MarkOccurencesSettingsNames.EXIT, true)
&& isIn(cu, info.getTrees().getSourcePositions(), type, caretPosition)) {
MethodExitDetector med = new MethodExitDetector();
setExitDetector(med);
try {
return med.process(info, doc, declPath, null);
} finally {
setExitDetector(null);
}
}
for (Tree exc : decl.getThrows()) {
if ( node.getBoolean(MarkOccurencesSettingsNames.EXCEPTIONS, true)
&& isIn(cu, info.getTrees().getSourcePositions(), exc, caretPosition)) {
MethodExitDetector med = new MethodExitDetector();
setExitDetector(med);
try {
return med.process(info, doc, declPath, Collections.singletonList(exc));
} finally {
setExitDetector(null);
}
}
}
}
if (isCancelled())
return null;
if (node.getBoolean(MarkOccurencesSettingsNames.EXCEPTIONS, true)) {
//detect caret inside catch:
if (typePath != null && ((typePath.getParentPath().getLeaf().getKind() == Kind.UNION_TYPE
&& typePath.getParentPath().getParentPath().getLeaf().getKind() == Kind.VARIABLE
&& typePath.getParentPath().getParentPath().getParentPath().getLeaf().getKind() == Kind.CATCH)
|| (typePath.getParentPath().getLeaf().getKind() == Kind.VARIABLE
&& typePath.getParentPath().getParentPath().getLeaf().getKind() == Kind.CATCH))) {
MethodExitDetector med = new MethodExitDetector();
setExitDetector(med);
try {
TreePath tryPath = tu.getPathElementOfKind(Kind.TRY, typePath);
if (tryPath != null) {
return med.process(info, doc, new TreePath(tryPath, ((TryTree)tryPath.getLeaf()).getBlock()), Collections.singletonList(typePath.getLeaf()));
}
} finally {
setExitDetector(null);
}
}
}
if (isCancelled())
return null;
if (node.getBoolean(MarkOccurencesSettingsNames.IMPLEMENTS, true) || node.getBoolean(MarkOccurencesSettingsNames.OVERRIDES, true)) {
//detect caret inside the extends/implements clause:
if (typePath != null && TreeUtilities.CLASS_TREE_KINDS.contains(typePath.getParentPath().getLeaf().getKind())) {
ClassTree ctree = (ClassTree) typePath.getParentPath().getLeaf();
int bodyStart = Utilities.findBodyStart(info, ctree, cu, info.getTrees().getSourcePositions(), doc);
boolean isExtends = ctree.getExtendsClause() == typePath.getLeaf();
boolean isImplements = false;
for (Tree t : ctree.getImplementsClause()) {
if (t == typePath.getLeaf()) {
isImplements = true;
break;
}
}
if ( (isExtends && node.getBoolean(MarkOccurencesSettingsNames.OVERRIDES, true))
|| (isImplements && node.getBoolean(MarkOccurencesSettingsNames.IMPLEMENTS, true))) {
Element superType = info.getTrees().getElement(typePath);
Element thisType = info.getTrees().getElement(typePath.getParentPath());
if (isClass(superType) && isClass(thisType))
return detectMethodsForClass(info, doc, typePath.getParentPath(), (TypeElement) superType, (TypeElement) thisType);
}
}
if (isCancelled())
return null;
TokenSequence<JavaTokenId> ts = info.getTokenHierarchy().tokenSequence(JavaTokenId.language());
if (ts != null && TreeUtilities.CLASS_TREE_KINDS.contains(tp.getLeaf().getKind())) {
int bodyStart = Utilities.findBodyStart(info, tp.getLeaf(), cu, info.getTrees().getSourcePositions(), doc);
if (caretPosition < bodyStart) {
ts.move(caretPosition);
if (ts.moveNext()) {
if (node.getBoolean(MarkOccurencesSettingsNames.OVERRIDES, true) && ts.token().id() == JavaTokenId.EXTENDS) {
Tree superClass = ((ClassTree) tp.getLeaf()).getExtendsClause();
if (superClass != null) {
Element superType = info.getTrees().getElement(new TreePath(tp, superClass));
Element thisType = info.getTrees().getElement(tp);
if (isClass(superType) && isClass(thisType))
return detectMethodsForClass(info, doc, tp, (TypeElement) superType, (TypeElement) thisType);
}
}
if (node.getBoolean(MarkOccurencesSettingsNames.IMPLEMENTS, true) && ts.token().id() == JavaTokenId.IMPLEMENTS) {
List<? extends Tree> superClasses = ((ClassTree) tp.getLeaf()).getImplementsClause();
if (superClasses != null) {
List<TypeElement> superTypes = new ArrayList<TypeElement>();
for (Tree superTypeTree : superClasses) {
if (superTypeTree != null) {
Element superType = info.getTrees().getElement(new TreePath(tp, superTypeTree));
if (isClass(superType))
superTypes.add((TypeElement) superType);
}
}
Element thisType = info.getTrees().getElement(tp);
if (!superTypes.isEmpty() && isClass(thisType))
return detectMethodsForClass(info, doc, tp, superTypes, (TypeElement) thisType);
}
}
}
}
}
}
if (isCancelled())
return null;
Tree tree =tp.getLeaf();
if (node.getBoolean(MarkOccurencesSettingsNames.BREAK_CONTINUE, true)) {
if (tree.getKind() == Kind.BREAK || tree.getKind() == Kind.CONTINUE) {
return detectBreakOrContinueTarget(info, doc, tp, caretPosition);
} else if (tree.getKind() == Kind.LABELED_STATEMENT) {
int[] span = Utilities.findIdentifierSpan(tp, info, doc);
if (span[0] <= caretPosition && caretPosition <= span[1]) {
List<int[]> ret = detectLabel(info, doc, tp);
ret.add(span);
return ret;
}
}
}
Element el;
el = JavadocImports.findReferencedElement(info, caretPosition);
boolean insideJavadoc = el != null;
if (isCancelled()) {
return null;
}
//variable declaration:
if (!insideJavadoc) {
if (tp.getParentPath() != null && tp.getParentPath().getLeaf().getKind() == Kind.NEW_CLASS) {
TreePath c = new TreePath(tp.getParentPath(), ((NewClassTree) tp.getParentPath().getLeaf()).getIdentifier());
if (isIn(caretPosition, Utilities.findIdentifierSpan(info, doc, c))) {
el = info.getTrees().getElement(tp.getParentPath());
} else {
el = info.getTrees().getElement(tp);
}
} else {
el = info.getTrees().getElement(tp);
}
}
if ( el != null
&& el.getKind() != ElementKind.MODULE
&& el.asType().getKind() != TypeKind.OTHER
&& (!TreeUtilities.CLASS_TREE_KINDS.contains(tree.getKind()) || isIn(caretPosition, Utilities.findIdentifierSpan(info, doc, tp)))
&& !Utilities.isNonCtorKeyword(tree)
&& (!(tree.getKind() == Kind.METHOD) || isIn(caretPosition, Utilities.findIdentifierSpan(info, doc, tp)))
&& isEnabled(node, el)
|| (insideJavadoc && isEnabled(node, el))) {
FindLocalUsagesQuery fluq = new FindLocalUsagesQuery();
setLocalUsages(fluq);
try {
List<int[]> bag = new ArrayList<int[]>();
for (Token t : fluq.findUsages(el, info, doc)) {
// type parameter name is represented as ident -> ignore surrounding <> in rename
int delta = t.id() == JavadocTokenId.IDENT && t.text().charAt(0) == '<' && t.text().charAt(t.length() - 1) == '>' ? 1 : 0;
bag.add(new int[] {t.offset(null) + delta, t.offset(null) + t.length() - delta});
}
return bag;
} finally {
setLocalUsages(null);
}
}
if (tp.getParentPath() != null && tp.getParentPath().getLeaf().getKind() == Kind.IMPORT) {
ImportTree it = (ImportTree) tp.getParentPath().getLeaf();
if (it.isStatic() && tp.getLeaf().getKind() == Kind.MEMBER_SELECT) {
MemberSelectTree mst = (MemberSelectTree) tp.getLeaf();
if (!"*".contentEquals(mst.getIdentifier())) {
List<int[]> bag = new ArrayList<int[]>();
Token<JavaTokenId> tok = Utilities.getToken(info, doc, tp);
if (tok != null)
bag.add(new int[] {tok.offset(null), tok.offset(null) + tok.length()});
el = info.getTrees().getElement(new TreePath(tp, mst.getExpression()));
if (el != null) {
FindLocalUsagesQuery fluq = new FindLocalUsagesQuery();
setLocalUsages(fluq);
try {
for (Element element : el.getEnclosedElements()) {
if (element.getModifiers().contains(Modifier.STATIC)) {
for (Token t : fluq.findUsages(element, info, doc)) {
bag.add(new int[] {t.offset(null), t.offset(null) + t.length()});
}
}
}
return bag;
} finally {
setLocalUsages(null);
}
}
}
}
}
return null;
}
private static final Set<Kind> TYPE_PATH_ELEMENT = EnumSet.of(Kind.IDENTIFIER, Kind.PRIMITIVE_TYPE, Kind.PARAMETERIZED_TYPE, Kind.MEMBER_SELECT, Kind.ARRAY_TYPE);
private static TreePath findTypePath(TreePath tp) {
if (!TYPE_PATH_ELEMENT.contains(tp.getLeaf().getKind()))
return null;
while (TYPE_PATH_ELEMENT.contains(tp.getParentPath().getLeaf().getKind())) {
tp = tp.getParentPath();
}
return tp;
}
private static boolean isClass(Element el) {
return el != null && (el.getKind().isClass() || el.getKind().isInterface());
}
private static boolean isEnabled(Preferences node, Element el) {
switch (el.getKind()) {
case ANNOTATION_TYPE:
case CLASS:
case ENUM:
case INTERFACE:
case TYPE_PARAMETER: //???
return node.getBoolean(MarkOccurencesSettingsNames.TYPES, true);
case CONSTRUCTOR:
case METHOD:
return node.getBoolean(MarkOccurencesSettingsNames.METHODS, true);
case ENUM_CONSTANT:
return node.getBoolean(MarkOccurencesSettingsNames.CONSTANTS, true);
case FIELD:
if (el.getModifiers().containsAll(EnumSet.of(Modifier.STATIC, Modifier.FINAL))) {
return node.getBoolean(MarkOccurencesSettingsNames.CONSTANTS, true);
} else {
return node.getBoolean(MarkOccurencesSettingsNames.FIELDS, true);
}
case LOCAL_VARIABLE:
case RESOURCE_VARIABLE:
case PARAMETER:
case EXCEPTION_PARAMETER:
return node.getBoolean(MarkOccurencesSettingsNames.LOCAL_VARIABLES, true);
case MODULE:
case PACKAGE:
return false; //never mark occurrence modules and packages
default:
Logger.getLogger(MarkOccurrencesHighlighterBase.class.getName()).log(Level.INFO, "Unknow element type: {0}.", el.getKind());
return true;
}
}
private boolean canceled;
private MethodExitDetector exitDetector;
private FindLocalUsagesQuery localUsages;
private final synchronized void setExitDetector(MethodExitDetector detector) {
this.exitDetector = detector;
}
private final synchronized void setLocalUsages(FindLocalUsagesQuery localUsages) {
this.localUsages = localUsages;
}
public final synchronized void cancel() {
canceled = true;
if (exitDetector != null) {
exitDetector.cancel();
}
if (localUsages != null) {
localUsages.cancel();
}
}
protected final synchronized boolean isCancelled() {
return canceled;
}
protected final synchronized void resume() {
canceled = false;
}
private List<int[]> detectMethodsForClass(CompilationInfo info, Document document, TreePath clazz, TypeElement superType, TypeElement thisType) {
return detectMethodsForClass(info, document, clazz, Collections.singletonList(superType), thisType);
}
private List<int[]> detectMethodsForClass(CompilationInfo info, Document document, TreePath clazz, List<TypeElement> superTypes, TypeElement thisType) {
List<int[]> highlights = new ArrayList<int[]>();
ClassTree clazzTree = (ClassTree) clazz.getLeaf();
TypeElement jlObject = info.getElements().getTypeElement("java.lang.Object");
OUTER: for (Tree member: clazzTree.getMembers()) {
if (isCancelled()) {
return null;
}
if (member.getKind() == Kind.METHOD) {
TreePath path = new TreePath(clazz, member);
Element el = info.getTrees().getElement(path);
if (el.getKind() == ElementKind.METHOD) {
for (TypeElement superType : superTypes) {
for (ExecutableElement ee : ElementFilter.methodsIn(info.getElements().getAllMembers(superType))) {
if (info.getElements().overrides((ExecutableElement) el, ee, thisType) && (superType.getKind().isClass() || !ee.getEnclosingElement().equals(jlObject))) {
Token t = Utilities.getToken(info, document, path);
if (t != null) {
highlights.add(new int[] {t.offset(null), t.offset(null) + t.length()});
}
continue OUTER;
}
}
}
}
}
}
return highlights;
}
private List<int[]> detectBreakOrContinueTarget(CompilationInfo info, Document document, TreePath breakOrContinue, int caretPosition) {
List<int[]> result = new ArrayList<int[]>();
Tree target = info.getTreeUtilities().getBreakContinueTargetTree(breakOrContinue);
if (target == null)
return null;
TokenSequence<JavaTokenId> ts = info.getTokenHierarchy().tokenSequence(JavaTokenId.language());
ts.move((int) info.getTrees().getSourcePositions().getStartPosition(info.getCompilationUnit(), target));
if (ts.moveNext()) {
result.add(new int[] {ts.offset(), ts.offset() + ts.token().length()});
}
StatementTree targetStatementTree = null;
ExpressionTree targetExpressionTree = null;
if (target instanceof StatementTree) {
targetStatementTree = target.getKind() == Kind.LABELED_STATEMENT ? ((LabeledStatementTree) target).getStatement() : (StatementTree) target;
} else if (target instanceof ExpressionTree) {
targetExpressionTree = (ExpressionTree) target;
}
Tree block = null;
if (targetStatementTree != null) {
switch (targetStatementTree.getKind()) {
case SWITCH:
block = targetStatementTree;
break;
case WHILE_LOOP:
if (((WhileLoopTree) targetStatementTree).getStatement().getKind() == Kind.BLOCK)
block = ((WhileLoopTree) targetStatementTree).getStatement();
break;
case FOR_LOOP:
if (((ForLoopTree) targetStatementTree).getStatement().getKind() == Kind.BLOCK)
block = ((ForLoopTree) targetStatementTree).getStatement();
break;
case ENHANCED_FOR_LOOP:
if (((EnhancedForLoopTree) targetStatementTree).getStatement().getKind() == Kind.BLOCK)
block = ((EnhancedForLoopTree) targetStatementTree).getStatement();
break;
case DO_WHILE_LOOP:
if (((DoWhileLoopTree) targetStatementTree).getStatement().getKind() == Kind.BLOCK)
block = ((DoWhileLoopTree) targetStatementTree).getStatement();
break;
}
// No need to check for version of JDK, as targetExpressionTree can only be non-null in case of JDK-12 or higher
} else if (targetExpressionTree != null) {
block = targetExpressionTree;
}
if (block != null) {
ts.move((int) info.getTrees().getSourcePositions().getEndPosition(info.getCompilationUnit(), block));
if (ts.movePrevious() && ts.token().id() == JavaTokenId.RBRACE) {
result.add(new int[] {ts.offset(), ts.offset() + ts.token().length()});
}
}
if (target.getKind() == Kind.LABELED_STATEMENT && isIn(caretPosition, Utilities.findIdentifierSpan(info, document, breakOrContinue))) {
result.addAll(detectLabel(info, document, info.getTrees().getPath(info.getCompilationUnit(), target)));
}
return result;
}
private List<int[]> detectLabel(final CompilationInfo info, final Document document, final TreePath labeledStatement) {
final List<int[]> result = new ArrayList<int[]>();
if (labeledStatement.getLeaf().getKind() == Kind.LABELED_STATEMENT) {
final Name label = ((LabeledStatementTree)labeledStatement.getLeaf()).getLabel();
new ErrorAwareTreePathScanner <Void, Void>() {
@Override
public Void visitBreak(BreakTree node, Void p) {
if (node.getLabel() != null && label.contentEquals(node.getLabel())) {
result.add(Utilities.findIdentifierSpan(getCurrentPath(), info, document));
}
return super.visitBreak(node, p);
}
@Override
public Void visitContinue(ContinueTree node, Void p) {
if (node.getLabel() != null && label.contentEquals(node.getLabel())) {
result.add(Utilities.findIdentifierSpan(getCurrentPath(), info, document));
}
return super.visitContinue(node, p);
}
}.scan(labeledStatement, null);
}
return result;
}
@Override
public int getPriority() {
return 100;
}
@Override
public Class<? extends Scheduler> getSchedulerClass() {
return Scheduler.CURSOR_SENSITIVE_TASK_SCHEDULER;
}
}