| /*=============================================================================# |
| # 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; |
| } |
| |
| } |