/*
* 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.refactoring.core.utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.eclipse.codebrowsing.fragments.ASTFragmentKind;
import org.codehaus.groovy.eclipse.codebrowsing.fragments.IASTFragment;
import org.codehaus.groovy.eclipse.codebrowsing.requestor.Region;
import org.codehaus.groovy.eclipse.codebrowsing.selection.FindSurroundingNode;
import org.codehaus.groovy.eclipse.codebrowsing.selection.FindSurroundingNode.VisitKind;
import org.codehaus.groovy.eclipse.core.compiler.GroovySnippetParser;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
/**
* Various AST related helpers.
*
* @author mike klenk
*/
public class ASTTools {
/**
* Returns a selection from the start of the first statement
* to the end of the last statement in the block statement.
*/
public static Region getPositionOfBlockStatements(BlockStatement block) {
if (!block.getStatements().isEmpty()) {
int start = getPositionOfStatement(block.getStatements().get(0)).getOffset(),
until = getPositionOfStatement(block.getStatements().get(block.getStatements().size() - 1)).getEnd();
if (start >= until) {
throw new IllegalStateException(String.format(
"Block statement start offset (%d) >= end offset (%d)%nfirst statement: %s%nlast statement: %s",
start, until, block.getStatements().get(0), block.getStatements().get(block.getStatements().size() - 1)));
}
return new Region(start, until - start);
}
return new Region(0, 0);
}
public static Region getPositionOfStatement(Statement statement) {
if (!hasValidPosition(statement) && statement instanceof ReturnStatement) {
ASTNode expression = ((ReturnStatement) statement).getExpression();
return new Region(expression.getStart(), expression.getLength());
}
return new Region(statement.getStart(), statement.getLength());
}
/**
* Returns true if the source location has been set to a valid value.
*/
public static boolean hasValidPosition(ASTNode node) {
return node.getEnd() > 0;
}
/**
* Removes the leading space (tabs/spaces) in front of the first character.
*/
public static String trimLeadingGap(String text) {
return text.replaceFirst("[ \t\f]*", "");
}
/**
* Returns the leading gap in front of the first character
*/
public static String getLeadingGap(String text) {
return text.replace(trimLeadingGap(text), "");
}
/**
* Returns the given String with a changed intentation. Existing intentation
* is replaced with space with the given modus.
*
* @param intentation
* given in "Tabs" <0 results in a movement on the left 0 has no
* effect >0 moves the text to the right
* @param modus
* fill the leading space with: ASTTools.SPACE or ASTTools.TAB.
* Tab is the default behaviour.
*/
public static String setIndentationTo(String text, int intentation, int modus) {
StringBuilder retString = new StringBuilder();
// Compile the pattern
String patternStr = ".+?(\n|\r\n|\r|\\z)";
Pattern pattern = Pattern.compile(patternStr, Pattern.DOTALL);
Matcher matcher = pattern.matcher(text);
// Read the lines
while (matcher.find()) {
// Get the line with the line termination character sequence
String line = matcher.group(0);
int currentIntetnation = getCurrentIntentation(line);
String space;
switch (modus) {
case SPACE:
space = " ";
break;
default:
space = "\t";
break;
}
for (int i = 0; i < (currentIntetnation + intentation); i++) {
retString.append(space);
}
retString.append(trimLeadingGap(line));
}
return retString.toString();
}
public static final int SPACE = 1;
public static final int TAB = 2;
/**
* Returns the current Intentation of this string measured in "Tabs".
*/
public static int getCurrentIntentation(String line) {
String leadingGap = getLeadingGap(line);
int tabs = 0, spaces = 0;
for (int i = 0, n = leadingGap.length(); i < n; i += 1) {
switch (leadingGap.charAt(i)) {
case '\t':
tabs += 1;
break;
case ' ':
spaces += 1;
break;
default:
break;
}
}
int currentIntetnation = tabs + (spaces / 4);
return currentIntetnation;
}
public static IDocument getDocumentWithSystemLineBreak(String text) {
Document document = new Document();
String linebreak = document.getDefaultLineDelimiter();
document.set(text);
try {
int lineCount = document.getNumberOfLines();
MultiTextEdit multiEdit = new MultiTextEdit();
for (int i = 0; i < lineCount; i++) {
final String delimiter = document.getLineDelimiter(i);
if (delimiter != null && delimiter.length() > 0 && !delimiter.equals(linebreak)) {
IRegion region = document.getLineInformation(i);
multiEdit.addChild(
new ReplaceEdit(region.getOffset() + region.getLength(), delimiter.length(), linebreak));
}
}
multiEdit.apply(document);
} catch (Exception e) {
}
return document;
}
public static ModuleNode getASTNodeFromSource(String source) {
GroovySnippetParser parser = new GroovySnippetParser();
ModuleNode node = parser.parse(source);
return node;
}
public static boolean hasMultipleReturnStatements(Statement statement) {
List<ReturnStatement> returns = new ArrayList<ReturnStatement>();
statement.visit(new FindReturns(returns));
return returns.size() > 1;
}
private static class FindReturns extends ASTVisitorDecorator<List<ReturnStatement>> {
public FindReturns(List<ReturnStatement> container) {
super(container);
}
@Override
public void visitReturnStatement(ReturnStatement statement) {
container.add(statement);
super.visitReturnStatement(statement);
}
}
public static String getTextofNode(ASTNode node, IDocument document) {
TextSelection sel = new TextSelection(document, node.getStart(), node.getEnd()-node.getStart());
try {
return document.get(sel.getOffset(), sel.getLength());
} catch (BadLocationException e) {
return "";
}
}
public static Set<Variable> getVariablesInScope(ModuleNode moduleNode, ASTNode node) {
FindSurroundingNode find = new FindSurroundingNode(new Region(node), VisitKind.PARENT_STACK);
find.doVisitSurroundingNode(moduleNode);
List<IASTFragment> parentStack = new ArrayList<IASTFragment>(find.getParentStack());
Collections.reverse(parentStack);
Set<Variable> vars = new HashSet<Variable>();
for (IASTFragment fragment : parentStack) {
ASTNode astNode = fragment.getAssociatedNode();
VariableScope scope;
if (astNode instanceof BlockStatement) {
scope = ((BlockStatement) astNode).getVariableScope();
} else if (astNode instanceof MethodNode) {
scope = ((MethodNode) astNode).getVariableScope();
} else if (astNode instanceof ClosureExpression) {
scope = ((ClosureExpression) astNode).getVariableScope();
} else {
scope = null;
}
if (scope != null) {
Iterator<Variable> declaredVariables = scope.getDeclaredVariablesIterator();
while (declaredVariables.hasNext()) {
vars.add(declaredVariables.next());
}
}
}
return vars;
}
public static ClassNode getContainingClassNode(ModuleNode moduleNode, int offset) {
ClassNode containingClassNode = null;
ClassNode scriptClass = null;
List<ClassNode> classes = moduleNode.getClasses();
for (ClassNode clazz : (Iterable<ClassNode>) classes) {
if (clazz.isScript()) {
scriptClass = clazz;
} else {
if (clazz.getStart() <= offset && clazz.getEnd() >= offset) {
containingClassNode = clazz;
}
}
}
if (containingClassNode == null) {
if (scriptClass != null && scriptClass.getStart() <= offset && scriptClass.getEnd() >= offset) {
containingClassNode = scriptClass;
} else {
// ensure this method never returns null
containingClassNode = moduleNode.getScriptClassDummy();
}
} else {
// look for inner classes
Iterator<InnerClassNode> innerClasses = containingClassNode.getInnerClasses();
while (innerClasses != null && innerClasses.hasNext()) {
InnerClassNode inner = innerClasses.next();
if (inner.getStart() <= offset && inner.getEnd() >= offset) {
containingClassNode = inner;
innerClasses = inner.getInnerClasses();
}
}
}
return containingClassNode;
}
public static IASTFragment getSelectionFragment(ModuleNode moduleNode, int selectionStart, int selectionLength) {
FindSurroundingNode finder =
new FindSurroundingNode(new Region(selectionStart, selectionLength), VisitKind.SURROUNDING_NODE);
IASTFragment selectionFragment = null;
IASTFragment fragment = finder.doVisitSurroundingNode(moduleNode);
if (ASTFragmentKind.isExpressionKind(fragment)) {
selectionFragment = fragment;
}
return selectionFragment;
}
}