| /*=============================================================================# |
| # Copyright (c) 2008, 2017 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 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.jface.text.IRegion; |
| 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.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.IRFrame; |
| import org.eclipse.statet.r.core.model.IRSourceUnit; |
| import org.eclipse.statet.r.core.model.RElementAccess; |
| import org.eclipse.statet.r.core.model.RElementName; |
| 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.RAst; |
| import org.eclipse.statet.r.core.rsource.ast.RAstNode; |
| import org.eclipse.statet.r.core.source.RHeuristicTokenScanner; |
| |
| public class InlineTempRefactoring extends Refactoring { |
| |
| |
| private final RRefactoringAdapter adapter= new RRefactoringAdapter(); |
| private final ElementSet elementSet; |
| |
| private IRegion selectionRegion; |
| |
| private final IRSourceUnit sourceUnit; |
| |
| private RAstNode symbolNode; |
| private RElementAccess[] accessList; |
| private 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 IRSourceUnit su, final IRegion region) { |
| this.sourceUnit= su; |
| this.elementSet= new ElementSet(new Object[] { su }); |
| |
| if (region != null && region.getOffset() >= 0 && region.getLength() >= 0) { |
| this.selectionRegion= region; |
| } |
| } |
| |
| |
| @Override |
| public String getName() { |
| return Messages.InlineTemp_label; |
| } |
| |
| public String getIdentifier() { |
| return RRefactoring.INLINE_TEMP_REFACTORING_ID; |
| } |
| |
| public int getReferencesCount() { |
| return (this.accessList != null) ? this.accessList.length : -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 IRFrame frame= currentAccess.getFrame(); |
| if (frame != null |
| && (frame.getFrameType() == IRFrame.PACKAGE || frame.getFrameType() == IRFrame.EXPLICIT)) { |
| result.merge(RefactoringStatus.createFatalErrorStatus(Messages.InlineTemp_error_InvalidSelectionNotLocal_message)); |
| return; |
| } |
| |
| final ImIdentityList<? extends RElementAccess> allAccess= ImCollections.toIdentityList( |
| currentAccess.getAllInUnit(false) ); |
| int current= allAccess.indexOf(currentAccess); |
| if (current < 0) { |
| throw new IllegalStateException(); |
| } |
| RElementAccess writeAccess= null; |
| while (current >= 0) { |
| final RElementAccess access= allAccess.get(current); |
| if (access.isWriteAccess()) { |
| writeAccess= access; |
| break; |
| } |
| current--; |
| } |
| if (writeAccess == null) { |
| result.merge(RefactoringStatus.createFatalErrorStatus(Messages.InlineTemp_error_MissingDefinition_message)); |
| return; |
| } |
| 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 (RAst.hasErrors(source)) { |
| result.merge(RefactoringStatus.createWarningStatus(Messages.InlineTemp_warning_ValueSyntaxError_message)); |
| } |
| |
| final int start= current; |
| current++; |
| while (current < allAccess.size()) { |
| if (allAccess.get(current).isWriteAccess()) { |
| break; |
| } |
| current++; |
| } |
| this.accessList= new RElementAccess[current - start]; |
| System.arraycopy(allAccess, start, this.accessList, 0, this.accessList.length); |
| this.assignmentNode= assignment; |
| } |
| |
| public String getVariableName() { |
| if (this.accessList != null) { |
| return this.accessList[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 RAstNode value= this.assignmentNode.getSourceChild(); |
| |
| this.sourceUnit.connect(m.newChild(1)); |
| try { |
| final AbstractDocument doc= this.sourceUnit.getDocument(m.newChild(1)); |
| |
| final String text= doc.get(value.getOffset(), value.getLength()); |
| final String text2= "(" + text + ")"; //$NON-NLS-1$ //$NON-NLS-2$ |
| |
| // delete/replace def (check parent) |
| final RAstNode parent= this.assignmentNode.getRParent(); |
| if (parent.getNodeType() == NodeType.BLOCK || parent.getNodeType() == NodeType.SOURCELINES) { |
| final RHeuristicTokenScanner scanner= this.adapter.getScanner(this.sourceUnit); |
| final IRegion assignmentRegion= this.adapter.expandWhitespaceBlock(doc, this.assignmentNode, scanner); |
| TextChangeCompatibility.addTextEdit(change, Messages.InlineTemp_Changes_DeleteAssignment_name, |
| new DeleteEdit(assignmentRegion.getOffset(), assignmentRegion.getLength()) ); |
| m.worked(4); |
| } |
| else { |
| final TextEdit edit= new ReplaceEdit(this.assignmentNode.getOffset(), this.assignmentNode.getLength(), |
| requireParentheses(this.assignmentNode, value) ? text2 : text ); |
| TextChangeCompatibility.addTextEdit(change, Messages.InlineTemp_Changes_ReplaceAssignment_name, edit); |
| m.worked(4); |
| } |
| |
| // replace refs |
| { for (int i= 1; i < this.accessList.length; i++) { |
| final RAstNode node= this.accessList[i].getNode(); |
| final TextEdit edit= new ReplaceEdit(node.getOffset(), 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; |
| } |
| |
| } |