Skip to content

Commit

Permalink
feature: change LocalVariable name
Browse files Browse the repository at this point in the history
  • Loading branch information
pvojtechovsky committed Mar 15, 2017
1 parent 2a28bba commit 7516392
Show file tree
Hide file tree
Showing 8 changed files with 937 additions and 0 deletions.
111 changes: 111 additions & 0 deletions src/main/java/spoon/refactoring/AbstractRenameRefactor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Copyright (C) 2006-2017 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.refactoring;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import spoon.SpoonException;
import spoon.reflect.declaration.CtNamedElement;
import spoon.reflect.reference.CtReference;
import spoon.reflect.visitor.chain.CtConsumer;

public abstract class AbstractRenameRefactor<T extends CtNamedElement> implements Refactor {
public static final Pattern javaIdentifierRE = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*");

protected T target;
protected String newName;
protected Pattern newNameValidationRE;

protected AbstractRenameRefactor(Pattern newNameValidationRE) {
this.newNameValidationRE = newNameValidationRE;
}

@Override
public void refactor() {
if (getTarget() == null) {
throw new SpoonException("The target of refactoring is not defined");
}
if (getNewName() == null) {
throw new SpoonException("The new name of refactoring is not defined");
}
List<Issue> issues = getIssues();
if (issues.isEmpty() == false) {
throw new SpoonException("Refactoring cannot be processed. There are issues: " + issues.toString());
}
refactorNoCheck();
}

protected void refactorNoCheck() {
forEachReference(new CtConsumer<CtReference>() {
@Override
public void accept(CtReference t) {
t.setSimpleName(AbstractRenameRefactor.this.newName);
}
});
target.setSimpleName(newName);
}

protected abstract void forEachReference(CtConsumer<CtReference> consumer);

@Override
public List<Issue> getIssues() {
List<Issue> issues = new ArrayList<>();
detectIssues(issues);
return issues;
}

protected void detectIssues(List<Issue> issues) {
checkNewNameIsValid(issues);
detectNameConflicts(issues);
}

/**
* checks whether {@link #newName} is valid java identifier
* @param issues
*/
protected void checkNewNameIsValid(List<Issue> issues) {
}

protected void detectNameConflicts(List<Issue> issues) {
}


protected boolean isJavaIdentifier(String name) {
return javaIdentifierRE.matcher(name).matches();
}

public T getTarget() {
return target;
}

public void setTarget(T target) {
this.target = target;
}

public String getNewName() {
return newName;
}

public void setNewName(String newName) {
if (newNameValidationRE != null && newNameValidationRE.matcher(newName).matches() == false) {
throw new SpoonException("New name \"" + newName + "\" is not valid name");
}
this.newName = newName;
}
}
238 changes: 238 additions & 0 deletions src/main/java/spoon/refactoring/ChangeLocalVariableName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/**
* Copyright (C) 2006-2017 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.refactoring;

import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;

import spoon.SpoonException;
import spoon.reflect.code.CtCatchVariable;
import spoon.reflect.code.CtLocalVariable;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtField;
import spoon.reflect.declaration.CtParameter;
import spoon.reflect.declaration.CtType;
import spoon.reflect.declaration.CtVariable;
import spoon.reflect.reference.CtFieldReference;
import spoon.reflect.reference.CtLocalVariableReference;
import spoon.reflect.reference.CtReference;
import spoon.reflect.reference.CtVariableReference;
import spoon.reflect.visitor.Filter;
import spoon.reflect.visitor.chain.CtConsumer;
import spoon.reflect.visitor.chain.CtQueryable;
import spoon.reflect.visitor.chain.CtScannerListener;
import spoon.reflect.visitor.chain.ScanningMode;
import spoon.reflect.visitor.filter.LocalVariableReferenceFunction;
import spoon.reflect.visitor.filter.LocalVariableScopeFunction;
import spoon.reflect.visitor.filter.PotentialVariableDeclarationFunction;
import spoon.reflect.visitor.filter.SiblingsFunction;
import spoon.reflect.visitor.filter.SiblingsFunction.Mode;
import spoon.reflect.visitor.filter.VariableReferenceFunction;

public class ChangeLocalVariableName extends AbstractRenameRefactor<CtLocalVariable<?>> {

public static Pattern validVariableNameRE = javaIdentifierRE;

public ChangeLocalVariableName() {
super(validVariableNameRE);
}

@Override
protected void forEachReference(CtConsumer<CtReference> consumer) {
getTarget().map(new VariableReferenceFunction()).forEach(consumer);
}

private static class QueryDriver implements CtScannerListener {
int nrOfNestedLocalClasses = 0;
CtElement ignoredParent;

@Override
public ScanningMode enter(CtElement element) {
if (ignoredParent != null && element instanceof CtElement) {
CtElement ele = (CtElement) element;
if (ele.hasParent(ignoredParent)) {
return ScanningMode.SKIP_ALL;
}
}
if (element instanceof CtType) {
nrOfNestedLocalClasses++;
}
return ScanningMode.NORMAL;
}

@Override
public void exit(CtElement element) {
if (ignoredParent == element) {
//we are living scope of ignored parent. We can stop checking it
ignoredParent = null;
}
if (element instanceof CtType) {
nrOfNestedLocalClasses--;
}
}

public void ignoreChildrenOf(CtElement element) {
if (ignoredParent != null) {
throw new SpoonException("Unexpected state. The ignoredParent is already set");
}
ignoredParent = element;
}

public boolean isInContextOfLocalClass() {
return nrOfNestedLocalClasses > 0;
}
}

@Override
protected void detectNameConflicts(final List<Issue> issues) {
/*
* There can be these conflicts
* 1) target variable would shadow before declared variable (parameter, localVariable, catchVariable)
* --------------------------------------------------------------------------------------------------
*/
PotentialVariableDeclarationFunction potentialDeclarationFnc = new PotentialVariableDeclarationFunction(newName);
CtVariable<?> var = getTarget().map(potentialDeclarationFnc).first();
if (var != null) {
if (var instanceof CtField) {
/*
* we have found a field of same name.
* It is not problem, because variables can hide field declaration.
* Do nothing - OK
*/
} else if (potentialDeclarationFnc.isTypeOnTheWay()) {
/*
* There is a local class declaration between future variable reference and variable declaration `var`.
* The found variable declaration `var` can be hidden by target variable with newName
* as long as there is no reference to `var` in visibility scope of the target variable.
* So search for such `var` reference now
*/
CtVariableReference<?> shadowedVar = target
.map(new SiblingsFunction().includingSelf(true).mode(Mode.NEXT))
.map(new VariableReferenceFunction(var)).first();
if (shadowedVar != null) {
//found variable reference, which would be shadowed by variable after rename.
createNameConflictIssue(issues, var, shadowedVar);
} else {
/*
* there is no local variable reference, which would be shadowed by variable after rename.
* OK
*/
}
} else {
/*
* the found variable is in conflict with target variable with newName
*/
createNameConflictIssue(issues, var);
}
}
/*
* 2) target variable is shadowed by later declared variable
* ---------------------------------------------------------
*/
final QueryDriver queryDriver = new QueryDriver();
getTarget().map(new LocalVariableScopeFunction(queryDriver)).select(new Filter<CtElement>() {
/**
* return true for all CtVariables, which are in conflict
*/
@Override
public boolean matches(CtElement element) {
if (element instanceof CtType<?>) {
CtType<?> localClass = (CtType<?>) element;
//TODO use faster hasField, implemented using map(new AllFieldsFunction()).select(new NameFilter(newName)).first()!=null
Collection<CtFieldReference<?>> fields = localClass.getAllFields();
for (CtFieldReference<?> fieldRef : fields) {
if (newName.equals(fieldRef.getSimpleName())) {
/*
* we have found a local class field, which will shadow input local variable if it's reference is in visibility scope of that field.
* Search for target variable reference in visibility scope of this field.
* If found than we cannot rename target variable to newName, because that reference would be shadowed
*/
queryDriver.ignoreChildrenOf(element);
CtLocalVariableReference<?> shadowedVar = element.map(new LocalVariableReferenceFunction(target)).first();
if (shadowedVar != null) {
createNameConflictIssue(issues, fieldRef.getFieldDeclaration(), shadowedVar);
return true;
}
return false;
}
}
return false;
}
if (element instanceof CtVariable<?>) {
CtVariable<?> variable = (CtVariable<?>) element;
if (newName.equals(variable.getSimpleName()) == false) {
//the variable with different name. Ignore it
return false;
}
//we have found a variable with new name
if (variable instanceof CtField) {
throw new SpoonException("This should not happen. The children of local class which contains a field with new name should be skipped!");
}
if (variable instanceof CtCatchVariable || variable instanceof CtLocalVariable || variable instanceof CtParameter) {
/*
* we have found a catch variable or local variable or parameter with new name.
*/
if (queryDriver.isInContextOfLocalClass()) {
/*
* We are in context of local class.
* This variable would shadow input local variable after rename
* so we cannot rename if there exist a local variable reference in variable visibility scope.
*/
queryDriver.ignoreChildrenOf(variable.getParent());
CtQueryable searchScope;
if (variable instanceof CtLocalVariable) {
searchScope = variable.map(new SiblingsFunction().includingSelf(true).mode(Mode.NEXT));
} else {
searchScope = variable.getParent();
}

CtLocalVariableReference<?> shadowedVar = searchScope.map(new LocalVariableReferenceFunction(target)).first();
if (shadowedVar != null) {
//found local variable reference, which would be shadowed by variable after rename.
createNameConflictIssue(issues, variable, shadowedVar);
return true;
}
//there is no local variable reference, which would be shadowed by variable after rename.
return false;
} else {
/*
* We are not in context of local class.
* So this variable is in conflict. Return it
*/
createNameConflictIssue(issues, variable);
return true;
}
} else {
//CtField should not be there, because the children of local class which contains a field with new name should be skipped!
//Any new variable type???
throw new SpoonException("Unexpected variable " + variable.getClass().getName());
}
}
return false;
}
}).first();
}

protected void createNameConflictIssue(List<Issue> issues, CtVariable<?> conflictVar) {
issues.add(new IssueImpl(conflictVar.getClass().getSimpleName() + " with name " + conflictVar.getSimpleName() + " is in conflict."));
}
protected void createNameConflictIssue(List<Issue> issues, CtVariable<?> conflictVar, CtVariableReference<?> shadowedVarRef) {
issues.add(new IssueImpl(conflictVar.getClass().getSimpleName() + " with name " + conflictVar.getSimpleName() + " would shadow local variable reference."));
}

}
21 changes: 21 additions & 0 deletions src/main/java/spoon/refactoring/Issue.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright (C) 2006-2017 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.refactoring;

public interface Issue {
String getMessage();
}
Loading

0 comments on commit 7516392

Please sign in to comment.