blob: 6628eddd94b2df0a72fc11c4bdfbad3327d5b291 [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.debugger.jpda.projects;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.CaseTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.lang.model.element.Element;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.TreePathHandle;
import org.netbeans.api.java.source.TreeUtilities;
import org.openide.filesystems.FileObject;
/**
* Refactoring inspired by IntroduceMethodFix, which creates a virtual class from a code snippet.
*
* @author martin
*/
class IntroduceClass {
private static final Logger LOG = Logger.getLogger(IntroduceClass.class.getName());
private String snippetCode;
private final int codeOffset;
private String methodBodyCode;
private String methodInvokeCode;
private long classGeneratePosition;
private final boolean staticContext;
IntroduceClass(String snippetCode, int codeOffset, boolean staticContext) {
this.snippetCode = snippetCode;
this.codeOffset = codeOffset;
this.staticContext = staticContext;
}
boolean computeIntroduceMethod(TreePathHandle h, CompilationInfo info, TreePath treePath, Tree tree) {
TreePath block = treePath;//h.resolve(info);
TreePath method = findMethod(block);
if (method == null) {
TreePath parentPath = treePath.getParentPath();
if (parentPath == null) {
return false;
}
method = parentPath;
}
CompilationUnitTree compilationUnit = info.getCompilationUnit();
SourcePositions sourcePositions = info.getTrees().getSourcePositions();
long endPosition = sourcePositions.getEndPosition(compilationUnit, method.getLeaf());
if (TreeUtilities.CLASS_TREE_KINDS.contains(method.getLeaf().getKind())) {
// We're in a class, generate before it's end:
this.classGeneratePosition = endPosition - 1;
} else {
// Can generate an inner class after this endPosition
this.classGeneratePosition = endPosition + 1;
}
List<? extends StatementTree> blockStatements = getStatements(block);
StatementTree lastStatement = blockStatements.isEmpty() ? null : blockStatements.get(blockStatements.size() - 1);
ScanLocalVars scanner = new ScanLocalVars(info, lastStatement);
scanner.scan(block, null);
Set<TypeMirror> exceptions = new HashSet<>();
String returnType = scanner.getReturnType();
if (returnType == null || !scanner.hasReturns()) {
TypeMirror type = scanner.getReturnTypeMirror();
if (type == null) {
Element lastStatementElement;
if (lastStatement != null && (lastStatementElement = getElement(info, new TreePath(treePath, lastStatement))) != null) {
type = lastStatementElement.asType();
if (TypeKind.EXECUTABLE.equals(type.getKind())) {
ExecutableType eType = (ExecutableType) type;
type = eType.getReturnType();
// Check that it ends with a semicolon:
long lsEnd = sourcePositions.getEndPosition(compilationUnit, lastStatement);
if (lsEnd < 0) {
lsEnd = this.snippetCode.length() - 1;
} else {
lsEnd -= codeOffset;
}
if (';' != this.snippetCode.charAt((int) lsEnd)) {
this.snippetCode = new StringBuilder(this.snippetCode)
.insert((int) lsEnd + 1, ";")
.toString();
}
}
}
}
if (type != null && !TypeKind.VOID.equals(type.getKind())) {
returnType = type.toString();
// Prepend a return statement:
long lsBegin = sourcePositions.getStartPosition(compilationUnit, lastStatement);
// Make it relative to the beginning of the code snippet:
lsBegin -= codeOffset;
StringBuilder code = new StringBuilder(this.snippetCode)
.insert((int) lsBegin, "return ");
if (!this.snippetCode.trim().endsWith(";")) {
code.append(';');
}
this.snippetCode = code.toString();
}
if (returnType == null) {
returnType = info.getTypes().getNoType(TypeKind.VOID).toString();
}
}
for (StatementTree s : blockStatements) {
TreePath path = new TreePath(treePath, s);
exceptions.addAll(info.getTreeUtilities().getUncaughtExceptions(path));
}
Set<VariableElement> referencedVariables = scanner.getReferencedVariables();
StringBuilder declaration = new StringBuilder(returnType);
declaration.append(" invoke(");
boolean isFirst = true;
for (VariableElement var : referencedVariables) {
if (!isFirst) {
declaration.append(", ");
}
declaration.append(var.asType().toString());
declaration.append(" ");
declaration.append(var.getSimpleName().toString());
isFirst = false;
}
declaration.append(") ");
if (!exceptions.isEmpty()) {
declaration.append("throws ");
isFirst = true;
for (TypeMirror exc : exceptions) {
if (!isFirst) {
declaration.append(", ");
}
declaration.append(exc.toString());
isFirst = false;
}
}
declaration.append("{\n");
declaration.append(snippetCode);
declaration.append("\n}");
this.methodBodyCode = declaration.toString();
StringBuilder methodInvode = new StringBuilder("invoke(");
isFirst = true;
for (VariableElement var : referencedVariables) {
if (!isFirst) {
methodInvode.append(", ");
}
methodInvode.append(var.getSimpleName().toString());
isFirst = false;
}
methodInvode.append(");");
this.methodInvokeCode = methodInvode.toString();
return true;
}
private static Element getElement(CompilationInfo info, TreePath path) {
Element elm = info.getTrees().getElement(path);
if (elm == null) {
if (path.getLeaf() instanceof ExpressionStatementTree) {
ExpressionStatementTree exp = (ExpressionStatementTree) path.getLeaf();
path = new TreePath(path, exp.getExpression());
elm = info.getTrees().getElement(path);
}
}
return elm;
}
String getMethodInvoke() {
return methodInvokeCode;
}
/**
* Returns a path to the immediate enclosing method, lambda body or initializer block
* @param path start of the search
* @return path to the nearest enclosing executable or {@code null} in case of error.
*/
static TreePath findMethod(TreePath path) {
while (path != null) {
Tree leaf = path.getLeaf();
switch (leaf.getKind()) {
case BLOCK:
if (path.getParentPath() != null && TreeUtilities.CLASS_TREE_KINDS.contains(path.getParentPath().getLeaf().getKind())) {
return path.getParentPath();
}
break;
case METHOD:
case LAMBDA_EXPRESSION:
return path;
default:
break;
}
path = path.getParentPath();
}
return null;
}
private static List<? extends StatementTree> getStatements(TreePath firstLeaf) {
Tree parentsLeaf = firstLeaf.getLeaf();
List<? extends StatementTree> statements;
switch (parentsLeaf.getKind()) {
case BLOCK:
statements = ((BlockTree) parentsLeaf).getStatements();
break;
case CASE:
statements = ((CaseTree) parentsLeaf).getStatements();
break;
default:
Tree first = firstLeaf.getLeaf();
if (Tree.Kind.EXPRESSION_STATEMENT.equals(first.getKind())) {
statements = Collections.singletonList((StatementTree) firstLeaf.getLeaf());
} else {
statements = Collections.emptyList();
}
}
int s = statements.size();
boolean haveOriginalStatements = true;
while (s > 0 && ";".equals(statements.get(--s).toString().trim())) {
if (haveOriginalStatements) {
statements = new ArrayList<>(statements);
haveOriginalStatements = false;
}
statements.remove(s);
}
return statements;
}
String computeIntroduceClass(String className, FileObject fo) throws IOException {
String fileText = fo.asText();
StringBuilder textBuilder = new StringBuilder(fileText);
String classText = ((this.staticContext) ? "static " : "") + "class "+className+" {\n"+
"public "+className+"() {}\n"+
this.methodBodyCode+"\n}";
textBuilder.insert((int) this.classGeneratePosition, classText);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Inserted class: '"+classText+"'");
LOG.fine("Updated full file content:\n'"+textBuilder.toString()+"'");
}
return textBuilder.toString();
}
}