/*
* Copyright 2009-2017 the original author or authors.
*
* Licensed 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.codehaus.groovy.eclipse.editor.highlighting;
import static org.eclipse.jdt.groovy.search.TypeLookupResult.TypeConfidence.UNKNOWN;
import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.GStringExpression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.MethodPointerExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.eclipse.editor.highlighting.HighlightedTypedPosition.HighlightKind;
import org.codehaus.jdt.groovy.model.GroovyCompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.groovy.core.util.GroovyUtils;
import org.eclipse.jdt.groovy.search.TypeLookupResult;
import org.eclipse.jdt.groovy.search.VariableScope;
import org.eclipse.jdt.internal.core.ImportDeclaration;
import org.eclipse.jdt.internal.core.SourceType;
import org.eclipse.jdt.internal.core.util.Util;
import org.eclipse.jface.text.Position;
/**
* Finds deprecated/unknown references, GString expressions, regular expressions,
* field/method/property references, static references, etc.
*/
public class SemanticHighlightingReferenceRequestor extends SemanticReferenceRequestor {
private static final Position NO_POSITION;
static {
NO_POSITION = new Position(0);
NO_POSITION.delete();
}
private char[] contents;
private final GroovyCompilationUnit unit;
private Position lastGString = NO_POSITION;
private static final boolean DEBUG = false;
/** Positions of interesting syntax elements within {@link #unit} in increasing lexical order. */
protected final SortedSet<HighlightedTypedPosition> typedPosition = new TreeSet<HighlightedTypedPosition>(new Comparator<HighlightedTypedPosition>() {
public int compare(HighlightedTypedPosition p1, HighlightedTypedPosition p2) {
int result = p1.compareTo(p2);
if (result == 0) {
// order matching positions by highlighting style
int x = p1.kind.ordinal(), y = p2.kind.ordinal();
result = (x < y) ? -1 : ((x == y) ? 0 : 1);
}
return result;
}
});
public SemanticHighlightingReferenceRequestor(GroovyCompilationUnit unit) {
this.unit = unit;
}
// be sure to call this before referencing contents array
private int unitLength() {
if (contents == null) {
contents = unit.getContents();
}
return contents.length;
}
public VisitStatus acceptASTNode(ASTNode node, TypeLookupResult result, IJavaElement enclosingElement) {
// ignore statements or nodes with invalid source locations
if (!(node instanceof AnnotatedNode) || node instanceof ImportNode || endOffset(node, result) < 1) {
if (DEBUG) System.err.println("skipping: " + node);
return VisitStatus.CONTINUE;
}
HighlightedTypedPosition pos = null;
if (result.confidence == UNKNOWN && node.getEnd() > 0) {
// GRECLIPSE-1327: check to see if this is a synthetic call() on a closure reference
if (isRealASTNode(node)) {
Position p = getPosition(node);
typedPosition.add(new HighlightedTypedPosition(p, HighlightKind.UNKNOWN));
// don't continue past an unknown reference
return VisitStatus.CANCEL_BRANCH;
}
} else if (isDeprecated(result.declaration)) {
pos = new HighlightedTypedPosition(getPosition(node), HighlightKind.DEPRECATED);
} else if (result.declaration instanceof FieldNode || result.declaration instanceof PropertyNode) {
pos = handleFieldOrProperty((AnnotatedNode) node, result.declaration);
} else if (node instanceof MethodNode) {
if (result.enclosingAnnotation == null) {
pos = handleMethodDeclaration((MethodNode) node);
} else {
pos = handleAnnotationElement(result.enclosingAnnotation, (MethodNode) node);
}
} else if (node instanceof ConstructorCallExpression) {
pos = handleMethodReference((ConstructorCallExpression) node);
} else if (node instanceof MethodCallExpression) {
pos = handleMethodReference((MethodCallExpression) node);
} else if (node instanceof StaticMethodCallExpression) {
pos = handleMethodReference((StaticMethodCallExpression) node);
} else if (node instanceof MethodPointerExpression) {
pos = handleMethodReference((MethodPointerExpression) node);
} else if (node instanceof Parameter) {
pos = handleVariableExpression((Parameter) node, result.scope);
} else if (node instanceof VariableExpression) {
if (result.declaration instanceof MethodNode) {
pos = handleMethodReference((Expression) node, result, false);
} else {
pos = handleVariableExpression((VariableExpression) node, result.scope, enclosingElement);
}
} else if (node instanceof ConstantExpression) {
if (result.declaration instanceof MethodNode) {
pos = handleMethodReference((Expression) node, result, (enclosingElement instanceof ImportDeclaration));
} else {
pos = handleConstantExpression((ConstantExpression) node);
}
} else if (node instanceof GStringExpression) {
pos = handleGStringExpression((GStringExpression) node);
} else if (node instanceof MapEntryExpression) {
pos = handleMapEntryExpression((MapEntryExpression) node);
} else if (DEBUG) {
String type = node.getClass().getSimpleName();
if (!type.matches("ClassNode|(Class|Binary|ArgumentList|Closure(List)?|Declaration|Property|List|Map)Expression"))
System.err.println("found: " + type);
}
if (pos != null && ((pos.getOffset() > 0 || pos.getLength() > 1) ||
// expression nodes can still be valid and have an offset of 0 and
// a length of 1, whereas for field/method nodes this is not allowed
node instanceof Expression)) {
typedPosition.add(pos);
}
return VisitStatus.CONTINUE;
}
// field and property declarations and references are handled the same
private HighlightedTypedPosition handleFieldOrProperty(AnnotatedNode node, ASTNode decl) {
HighlightKind kind;
if (!isStatic(decl)) {
kind = HighlightKind.FIELD;
} else if (!isFinal(decl)) {
kind = HighlightKind.STATIC_FIELD;
} else /* static & final */ {
kind = HighlightKind.STATIC_VALUE;
}
int offset, length;
if (node == decl) {
// declaration length includes the type and init
offset = node.getNameStart();
length = node.getNameEnd() - node.getNameStart() + 1;
} else {
offset = node.getStart();
length = node.getLength();
}
return new HighlightedTypedPosition(offset, length, kind);
}
private HighlightedTypedPosition handleAnnotationElement(AnnotationNode anno, MethodNode elem) {
try {
int start = GroovyUtils.startOffset(anno), until = GroovyUtils.endOffset(anno);
String source = unit.getSource().substring(start, until);
// search for the element label in the source since no AST node exists for it
Matcher m = Pattern.compile("\\b\\Q" + elem.getName() + "\\E\\b").matcher(source);
if (m.find()) {
return new HighlightedTypedPosition(start + m.start(), elem.getName().length(), HighlightKind.TAG_KEY);
}
} catch (Exception e) {
Util.log(e);
}
return null;
}
private HighlightedTypedPosition handleMethodDeclaration(MethodNode node) {
HighlightKind kind;
if (node instanceof ConstructorNode) {
kind = HighlightKind.CTOR;
} else if (!isStatic(node)) {
kind = HighlightKind.METHOD;
} else {
kind = HighlightKind.STATIC_METHOD;
}
int offset = node.getNameStart(),
length = node.getNameEnd() - node.getNameStart() + 1;
// special case: string literal method names
if (length > node.getName().length()) return null;
return new HighlightedTypedPosition(offset, length, kind);
}
private HighlightedTypedPosition handleMethodReference(MethodCallExpression expr) {
HighlightKind kind = HighlightKind.METHOD_CALL;
if (expr.getObjectExpression() instanceof ClassExpression) kind = HighlightKind.STATIC_CALL;
int offset = expr.getMethod().getStart(),
length = expr.getMethod().getLength();
return new HighlightedTypedPosition(offset, length, kind);
}
private HighlightedTypedPosition handleMethodReference(ConstructorCallExpression expr) {
if (expr.isSpecialCall()) {
return null; // handled by GroovyTagScanner
}
int offset = expr.getNameStart(),
length = expr.getNameEnd() - expr.getNameStart() + 1;
return new HighlightedTypedPosition(offset, length, HighlightKind.CTOR_CALL);
}
private HighlightedTypedPosition handleMethodReference(StaticMethodCallExpression expr) {
int offset = expr.getStart(),
length = expr.getMethod().length();
return new HighlightedTypedPosition(offset, length, HighlightKind.STATIC_CALL);
}
private HighlightedTypedPosition handleMethodReference(MethodPointerExpression expr) {
HighlightKind kind = !(expr.getExpression() instanceof ClassExpression)
? HighlightKind.METHOD_CALL : HighlightKind.STATIC_CALL;
int offset = expr.getMethodName().getStart(),
length = expr.getMethodName().getLength();
return new HighlightedTypedPosition(offset, length, kind);
}
private HighlightedTypedPosition handleMethodReference(Expression expr, TypeLookupResult result, boolean isStaticImport) {
MethodNode meth = (MethodNode) result.declaration;
HighlightKind kind = null;
if (result.isGroovy) {
kind = HighlightKind.GROOVY_CALL;
} else if (isStaticImport) {
kind = HighlightKind.STATIC_CALL;
} else if (!expr.getText().equals(meth.getName())) {
// property name did not match method name
// there won't be a [Static]MethodCallExpression
kind = !meth.isStatic() ? HighlightKind.METHOD_CALL : HighlightKind.STATIC_CALL;
}
if (kind != null) {
return new HighlightedTypedPosition(expr.getStart(), expr.getLength(), kind);
}
return null;
}
private HighlightedTypedPosition handleVariableExpression(Parameter expr, VariableScope scope) {
HighlightKind kind = HighlightKind.PARAMETER;
if (isCatchParam(expr, scope) || isForLoopParam(expr, scope)) {
kind = HighlightKind.VARIABLE; // treat block params as vars
}
return new HighlightedTypedPosition(expr.getNameStart(), expr.getNameEnd() - expr.getNameStart(), kind);
}
// could be local variable declaration, local variable reference, for-each parameter reference, or method parameter reference
private HighlightedTypedPosition handleVariableExpression(VariableExpression expr, VariableScope scope, IJavaElement source) {
boolean isParam = (expr.getAccessedVariable() instanceof Parameter &&
!isForLoopParam(expr.getAccessedVariable(), scope)) &&
!isCatchParam(expr.getAccessedVariable(), scope);
boolean isIt = (isParam && "it".equals(expr.getName()) &&
(((Parameter) expr.getAccessedVariable()).getLineNumber() <= 0));
boolean isSuperOrThis = "super".equals(expr.getName()) || "this".equals(expr.getName());
// free vars and loop vars are okay as long as they are not reserved words (this, super); params must refer to "real" declarations
if (!isSuperOrThis && (!isParam || isIt || (((Parameter) expr.getAccessedVariable()).getLineNumber() > 0) || source instanceof SourceType)) {
HighlightKind kind = isParam ? (isIt ? HighlightKind.GROOVY_CALL : HighlightKind.PARAMETER) : HighlightKind.VARIABLE;
return new HighlightedTypedPosition(expr.getStart(), expr.getLength(), kind);
}
return null;
}
private HighlightedTypedPosition handleConstantExpression(ConstantExpression expr) {
int offset = expr.getStart(),
length = expr.getLength();
HighlightedTypedPosition pos = null;
if (!lastGString.includes(offset)) {
if (isNumber(expr.getType())) {
pos = new HighlightedTypedPosition(offset, length, HighlightKind.NUMBER);
} else if (expr.getEnd() <= unitLength()) {
// check for /.../ or $/.../$ form of string literal (usually a regex literal)
boolean slashy = contents[offset] == '/' && contents[expr.getEnd() - 1] == '/';
boolean dollar = !slashy && contents[offset] == '$' && contents[offset + 1] == '/' &&
contents[expr.getEnd() - 2] == '/' && contents[expr.getEnd() - 1] == '$';
if (slashy || dollar) {
pos = new HighlightedTypedPosition(offset, length, HighlightKind.REGEXP);
}
}
}
return pos;
}
private HighlightedTypedPosition handleGStringExpression(GStringExpression expr) {
int offset = expr.getStart(),
length = expr.getLength();
// save to help deal with forthcoming ConstantExpression nodes
lastGString = new Position(offset, length);
// check for slashy form of GString
if (expr.getEnd() <= unitLength()) {
String source = String.valueOf(contents, offset, length);
if ((source.startsWith("/") && source.endsWith("/")) ||
(source.startsWith("$/") && source.endsWith("/$"))) {
return new HighlightedTypedPosition(lastGString, HighlightKind.REGEXP);
}
}
return null;
}
private HighlightedTypedPosition handleMapEntryExpression(MapEntryExpression expr) {
Expression key = expr.getKeyExpression();
if (key instanceof ConstantExpression) {
unitLength(); // ensure loaded
char c = contents[key.getStart()];
if (c != '\'' && c != '"' && c != '/') {
return new HighlightedTypedPosition(key.getStart(), key.getLength(), HighlightKind.MAP_KEY);
}
}
return null;
}
private int endOffset(ASTNode node, TypeLookupResult result) {
int offset = node.getEnd();
if (result.enclosingAnnotation != null) {
offset = result.enclosingAnnotation.getEnd();
// TODO: Probably could be more accurate, but doesn't need to be at the moment...
}
return offset;
}
/**
* An AST node is "real" if it is an expression and the
* text of the expression matches the actual text in the file
*/
private boolean isRealASTNode(ASTNode node) {
String text = node.getText();
if (text.length() != node.getLength()) {
return false;
}
int contentsLength = unitLength();
char[] textArr = text.toCharArray();
for (int i = 0, j = node.getStart(); i < textArr.length && j < contentsLength; i++, j++) {
if (textArr[i] != contents[j]) {
return false;
}
}
return true;
}
}