blob: a910f5d0d667f64e467e4b7066e459249d0b08ae [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2008, 2021 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.r.core.refactoring;
import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jface.text.AbstractDocument;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.osgi.util.NLS;
import org.eclipse.text.edits.DeleteEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.collections.ImIdentityList;
import org.eclipse.statet.jcommons.collections.ImList;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.text.core.TextRegion;
import org.eclipse.statet.internal.r.core.refactoring.Messages;
import org.eclipse.statet.ltk.core.Ltk;
import org.eclipse.statet.ltk.model.core.ElementSet;
import org.eclipse.statet.ltk.refactoring.core.CommonRefactoringDescriptor;
import org.eclipse.statet.ltk.refactoring.core.RefactoringChange;
import org.eclipse.statet.ltk.refactoring.core.RefactoringMessages;
import org.eclipse.statet.ltk.refactoring.core.SourceUnitChange;
import org.eclipse.statet.ltk.refactoring.core.TextChangeCompatibility;
import org.eclipse.statet.r.core.RCore;
import org.eclipse.statet.r.core.RUtil;
import org.eclipse.statet.r.core.model.RElementAccess;
import org.eclipse.statet.r.core.model.RElementName;
import org.eclipse.statet.r.core.model.RFrame;
import org.eclipse.statet.r.core.model.RSourceUnit;
import org.eclipse.statet.r.core.rsource.ast.Assignment;
import org.eclipse.statet.r.core.rsource.ast.NodeType;
import org.eclipse.statet.r.core.rsource.ast.RAstNode;
import org.eclipse.statet.r.core.rsource.ast.RAsts;
import org.eclipse.statet.r.core.source.RHeuristicTokenScanner;
@NonNullByDefault
public class InlineTempRefactoring extends Refactoring {
private final RRefactoringAdapter adapter= new RRefactoringAdapter();
private final ElementSet elementSet;
private final @Nullable TextRegion selectionRegion;
private final RSourceUnit sourceUnit;
private @Nullable RAstNode symbolNode;
/** [0]= write/assignment, [>= 1] read */
private @Nullable ImList<? extends RElementAccess> accessList;
private @Nullable Assignment assignmentNode;
/**
* Creates a new inline constant refactoring.
* @param su the source unit
* @param region (selected) region of an occurrence of the variable
*/
public InlineTempRefactoring(final RSourceUnit su, final TextRegion region) {
this.sourceUnit= su;
this.elementSet= new ElementSet(new Object[] { su });
this.selectionRegion= (region != null && region.getStartOffset() >= 0 && region.getLength() >= 0) ?
region : null;
}
@Override
public String getName() {
return Messages.InlineTemp_label;
}
public String getIdentifier() {
return RRefactoring.INLINE_TEMP_REFACTORING_ID;
}
public int getReferencesCount() {
final ImList<? extends RElementAccess> accessList= this.accessList;
return (accessList != null) ? accessList.size() : -1;
}
@Override
public RefactoringStatus checkInitialConditions(final IProgressMonitor monitor) throws CoreException {
final SubMonitor m= SubMonitor.convert(monitor, 6);
try {
if (this.selectionRegion != null) {
this.symbolNode= this.adapter.searchPotentialNameNode(this.sourceUnit,
this.selectionRegion, true, m.newChild(4) );
}
if (this.symbolNode == null) {
return RefactoringStatus.createFatalErrorStatus(Messages.InlineTemp_error_InvalidSelection_message);
}
final RefactoringStatus result= new RefactoringStatus();
this.adapter.checkInitialToModify(result, this.elementSet);
if (result.hasFatalError()) {
return result;
}
checkVariable(result);
return result;
}
finally {
m.done();
}
}
private void checkVariable(final RefactoringStatus result) {
final RElementAccess currentAccess= RElementAccess.getMainElementAccessOfNameNode(this.symbolNode);
if (currentAccess == null) {
result.merge(RefactoringStatus.createFatalErrorStatus("Failed to detect variable information."));
return;
}
if (currentAccess.getType() != RElementName.MAIN_DEFAULT || currentAccess.getNextSegment() != null) {
result.merge(RefactoringStatus.createFatalErrorStatus(Messages.InlineTemp_error_InvalidSelection_message)); // no common variable
return;
}
final RFrame frame= currentAccess.getFrame();
if (frame != null
&& (frame.getFrameType() == RFrame.PACKAGE || frame.getFrameType() == RFrame.EXPLICIT)) {
result.merge(RefactoringStatus.createFatalErrorStatus(Messages.InlineTemp_error_InvalidSelectionNotLocal_message));
return;
}
final ImIdentityList<? extends RElementAccess> allAccess= ImCollections.toIdentityList(
currentAccess.getAllInUnit(false) );
// write access
final int writeAccessIdx;
RElementAccess writeAccess= null;
{ int idx= allAccess.indexOf(currentAccess);
if (idx < 0) {
throw new IllegalStateException();
}
while (idx >= 0) {
final RElementAccess access= allAccess.get(idx);
if (access.isWriteAccess()) {
writeAccess= access;
break;
}
idx--;
}
if (writeAccess != null) {
writeAccessIdx= idx;
}
else {
result.merge(RefactoringStatus.createFatalErrorStatus(Messages.InlineTemp_error_MissingDefinition_message));
return;
}
}
// assignment
final RAstNode node= writeAccess.getNode();
switch (node != null ? node.getNodeType() : NodeType.DUMMY) {
case A_LEFT:
case A_RIGHT:
case A_EQUALS:
break;
case F_DEF_ARG:
result.merge(RefactoringStatus.createFatalErrorStatus(Messages.InlineTemp_error_InvalidSelectionParameter_message));
return;
default:
result.merge(RefactoringStatus.createFatalErrorStatus(Messages.InlineTemp_error_InvalidSelectionNoArrow_message));
return;
}
final Assignment assignment= (Assignment) node;
final RAstNode source= assignment.getSourceChild();
if (RAsts.hasErrors(source)) {
result.merge(RefactoringStatus.createWarningStatus(Messages.InlineTemp_warning_ValueSyntaxError_message));
}
// all relevant access
final int allAccessEnd;
{ int idx= writeAccessIdx + 1;
while (idx < allAccess.size()) {
if (allAccess.get(idx).isWriteAccess()) {
break;
}
idx++;
}
allAccessEnd= idx;
}
this.accessList= ImCollections.toList(allAccess.subList(writeAccessIdx, allAccessEnd));
this.assignmentNode= assignment;
}
public @Nullable String getVariableName() {
final ImList<? extends RElementAccess> accessList= this.accessList;
if (accessList != null) {
return accessList.get(0).getSegmentName();
}
return null;
}
@Override
public RefactoringStatus checkFinalConditions(final IProgressMonitor monitor) throws CoreException {
final SubMonitor m= SubMonitor.convert(monitor, RefactoringMessages.Common_FinalCheck_label, 3);
try {
final RefactoringStatus status= new RefactoringStatus();
this.adapter.checkFinalToModify(status, this.elementSet, m.newChild(2));
return status;
}
finally {
m.done();
}
}
@Override
public Change createChange(final IProgressMonitor monitor) throws CoreException {
final SubMonitor m= SubMonitor.convert(monitor, RefactoringMessages.Common_CreateChanges_label, 3);
try {
final TextFileChange textFileChange= new SourceUnitChange(this.sourceUnit);
if (this.sourceUnit.getWorkingContext() == Ltk.EDITOR_CONTEXT) {
textFileChange.setSaveMode(TextFileChange.LEAVE_DIRTY);
}
createChanges(textFileChange, m.newChild(2));
final Map<String, String> arguments= new HashMap<>();
final String description= NLS.bind(Messages.InlineTemp_Descriptor_description,
RUtil.formatVarName(getVariableName()) );
final IProject resource= this.elementSet.getSingleProject();
final String project= (resource != null) ? resource.getName() : null;
final String source= (project != null) ? NLS.bind(RefactoringMessages.Common_Source_Project_label, project) : RefactoringMessages.Common_Source_Workspace_label;
final int flags= 0;
final String comment= ""; //$NON-NLS-1$
final CommonRefactoringDescriptor descriptor= new CommonRefactoringDescriptor(
getIdentifier(), project, description, comment, arguments, flags);
m.worked(1);
return new RefactoringChange(descriptor,
Messages.InlineTemp_label,
new Change[] { textFileChange });
}
catch (final BadLocationException e) {
throw new CoreException(new Status(IStatus.ERROR, RCore.BUNDLE_ID, "Unexpected error (concurrent change?)", e));
}
finally {
m.done();
}
}
private void createChanges(final TextFileChange change, final SubMonitor m) throws BadLocationException {
m.setWorkRemaining(3 + 2 * 4);
final ImList<? extends RElementAccess> accessList= nonNullAssert(this.accessList);
final Assignment assignmentNode= nonNullAssert(this.assignmentNode);
final RAstNode value= assignmentNode.getSourceChild();
this.sourceUnit.connect(m.newChild(1));
try {
final AbstractDocument doc= this.sourceUnit.getDocument(m.newChild(1));
final String text= doc.get(value.getStartOffset(), value.getLength());
final String text2= "(" + text + ")"; //$NON-NLS-1$ //$NON-NLS-2$
// delete/replace def (check parent)
final RAstNode parent= assignmentNode.getRParent();
if (parent.getNodeType() == NodeType.BLOCK || parent.getNodeType() == NodeType.SOURCELINES) {
final RHeuristicTokenScanner scanner= this.adapter.getScanner(this.sourceUnit);
final TextRegion assignmentRegion= this.adapter.expandWhitespaceBlock(doc, assignmentNode, scanner);
TextChangeCompatibility.addTextEdit(change, Messages.InlineTemp_Changes_DeleteAssignment_name,
new DeleteEdit(assignmentRegion.getStartOffset(), assignmentRegion.getLength()) );
m.worked(4);
}
else {
final TextEdit edit= new ReplaceEdit(assignmentNode.getStartOffset(), assignmentNode.getLength(),
requireParentheses(assignmentNode, value) ? text2 : text );
TextChangeCompatibility.addTextEdit(change, Messages.InlineTemp_Changes_ReplaceAssignment_name, edit);
m.worked(4);
}
// replace refs
{ for (int i= 1; i < accessList.size(); i++) {
final RAstNode node= accessList.get(i).getNode();
final TextEdit edit= new ReplaceEdit(node.getStartOffset(), node.getLength(),
requireParentheses(node, value) ? text2 : text);
TextChangeCompatibility.addTextEdit(change, Messages.InlineTemp_Changes_ReplaceReference_name, edit);
}
m.worked(4);
}
}
finally {
this.sourceUnit.disconnect(m.newChild(1));
}
}
private boolean requireParentheses(final RAstNode oldValue, final RAstNode newValue) {
final RAstNode parent= oldValue.getRParent();
if (parent != null) {
return ((parent.getNodeType().opPrec > 15)
&& (parent.getNodeType().opPrec < newValue.getNodeType().opPrec) );
}
return false;
}
}