blob: fd7ee82d3e3467b914da9b807726c76c89fb8a8f [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2010, 2019 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.debug.core.breakpoints;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
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.jface.text.AbstractDocument;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IRegion;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.internal.r.debug.core.RDebugCorePlugin;
import org.eclipse.statet.internal.r.debug.core.breakpoints.RGenericLineBreakpoint;
import org.eclipse.statet.internal.r.debug.core.breakpoints.RGenericLineBreakpoint.CachedData;
import org.eclipse.statet.internal.r.debug.core.breakpoints.RLineBreakpoint;
import org.eclipse.statet.internal.r.debug.core.breakpoints.RMethodBreakpoint;
import org.eclipse.statet.ltk.ast.core.AstNode;
import org.eclipse.statet.ltk.ast.core.util.AstSelection;
import org.eclipse.statet.ltk.core.LTKUtils;
import org.eclipse.statet.ltk.model.core.IModelManager;
import org.eclipse.statet.ltk.model.core.elements.IModelElement;
import org.eclipse.statet.ltk.model.core.elements.ISourceStructElement;
import org.eclipse.statet.r.console.core.RDbg;
import org.eclipse.statet.r.core.model.IRElement;
import org.eclipse.statet.r.core.model.IRLangSourceElement;
import org.eclipse.statet.r.core.model.IRModelInfo;
import org.eclipse.statet.r.core.model.IRWorkspaceSourceUnit;
import org.eclipse.statet.r.core.model.RModel;
import org.eclipse.statet.r.core.rsource.ast.FDef;
import org.eclipse.statet.r.core.rsource.ast.GenericVisitor;
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.IRDocumentConstants;
import org.eclipse.statet.r.core.source.RHeuristicTokenScanner;
import org.eclipse.statet.r.debug.core.RDebugModel;
import org.eclipse.statet.r.nico.IRSrcref;
import org.eclipse.statet.r.nico.RSrcref;
public class RLineBreakpointValidator {
public static class ModelPosition {
private final RGenericLineBreakpoint.CachedData fData;
private ModelPosition(final RGenericLineBreakpoint.CachedData data) {
this.fData= data;
}
public String getElementId() {
return this.fData.getElementId();
}
public int[] getRExpressionIndex() {
return this.fData.getRExpressionIndex();
}
}
public static ModelPosition getModelPosition(final IRLineBreakpoint breakpoint) {
if (breakpoint instanceof RLineBreakpoint) {
final RLineBreakpoint internal= (RLineBreakpoint) breakpoint;
final CachedData cachedData= internal.getCachedData();
if (cachedData != null) {
return new ModelPosition(cachedData);
}
}
return null;
}
private static final int LINE_TOLERANCE= 5;
private static final String TOPLEVEL_ELEMENT_ID= "200:"; // Integer.toHexString(IModelElement.C1_SOURCE) + ':' //$NON-NLS-1$
private final IRWorkspaceSourceUnit sourceUnit;
private final AbstractDocument document;
private IRModelInfo modelInfo;
private String type;
private int originalLine;
private int line;
private int startOffset;
private int endOffset;
private IRLangSourceElement methodElement; // the deepest method element
private IRLangSourceElement baseElement; // main element, null for script list
private RAstNode astNode;
private RAstNode baseExpressionRootNode; // the root for the R expression index
public RLineBreakpointValidator(final IRWorkspaceSourceUnit su, final String type,
final int offset, final IProgressMonitor monitor) {
this.sourceUnit= su;
if (!initType(type)
|| this.sourceUnit.getResource().getType() != IResource.FILE ) {
this.document= null;
setInvalid();
return;
}
this.document= this.sourceUnit.getDocument(monitor);
this.originalLine= this.line= this.startOffset= this.endOffset= -1;
check(offset, monitor);
}
public RLineBreakpointValidator(final IRWorkspaceSourceUnit su, final IRLineBreakpoint breakpoint,
final IProgressMonitor monitor) throws CoreException {
this.sourceUnit= su;
if (!initType(breakpoint.getBreakpointType())
|| this.sourceUnit.getResource().getType() != IResource.FILE ) {
this.document= null;
setInvalid();
return;
}
this.document= this.sourceUnit.getDocument(monitor);
this.originalLine= this.line= this.startOffset= this.endOffset= -1;
final int offset= breakpoint.getCharStart();
check(offset, monitor);
if (this.type != null && breakpoint instanceof RGenericLineBreakpoint && su.isSynchronized()) {
((RGenericLineBreakpoint) breakpoint).setCachedData(new CachedData(
this.modelInfo.getStamp().getSourceStamp(), computeElementId(), computeRExpressionIndex() ));
}
}
private boolean initType(final String type) {
if (type == null) {
return true;
}
if (type.equals(RDebugModel.R_LINE_BREAKPOINT_TYPE_ID)) {
this.type= RDebugModel.R_LINE_BREAKPOINT_TYPE_ID;
return true;
}
else if (type.equals(RDebugModel.R_METHOD_BREAKPOINT_TYPE_ID)) {
this.type= RDebugModel.R_METHOD_BREAKPOINT_TYPE_ID;
return true;
}
else {
return false;
}
}
private void check(final int offset, final IProgressMonitor monitor) {
try {
this.line= this.originalLine= this.document.getLineOfOffset(offset);
this.methodElement= searchMethodElement(offset, monitor);
if (this.type == null) { // best
if (this.methodElement != null
&& this.document.getLineOfOffset(this.methodElement.getSourceRange().getStartOffset()) == this.line ) {
this.type= RDebugModel.R_METHOD_BREAKPOINT_TYPE_ID;
}
else {
this.type= RDebugModel.R_LINE_BREAKPOINT_TYPE_ID;
}
}
if (this.type == RDebugModel.R_LINE_BREAKPOINT_TYPE_ID) {
IRegion lineInformation= this.document.getLineInformation(this.line);
final RHeuristicTokenScanner scanner= RHeuristicTokenScanner.create(
this.sourceUnit.getDocumentContentInfo() );
scanner.configure(this.document, IRDocumentConstants.R_CODE_CONTENT_CONSTRAINT);
{ final IRegion lastLineInformation= this.document.getLineInformation(
Math.min(this.line + LINE_TOLERANCE, this.document.getNumberOfLines()-1) );
this.startOffset= scanner.findNonBlankForward(
lineInformation.getOffset(),
lastLineInformation.getOffset() + lastLineInformation.getLength(),
true );
}
if (this.startOffset < 0) {
setInvalid();
return;
}
this.astNode= searchSuspendAstNode(this.startOffset, monitor);
if (this.astNode == null) {
setInvalid();
return;
}
this.startOffset= this.astNode.getStartOffset();
if (this.startOffset < 0) {
setInvalid();
return;
}
this.line= this.document.getLineOfOffset(this.startOffset);
if (this.line != this.originalLine) {
lineInformation= this.document.getLineInformation(this.line);
}
this.endOffset= scanner.findNonBlankBackward(
lineInformation.getOffset() + lineInformation.getLength(),
this.startOffset - 1, true);
if (this.endOffset < 0) { // should never happen
setInvalid();
return;
}
if (this.methodElement != null
&& this.methodElement.getSourceRange().getStartOffset() != this.startOffset) {
this.baseElement= searchBaseElement(this.methodElement);
if (this.baseElement == null) {
setInvalid();
return;
}
this.baseExpressionRootNode= this.baseElement.getAdapter(FDef.class).getContChild();
if (!isBaseExpressionRootNodeValid()) {
// new Status(IStatus.ERROR, RDebugCorePlugin.BUNDLE_ID, "Only in blocks.");
setInvalid();
return;
}
}
else { // script line
this.baseExpressionRootNode= this.astNode.getRRoot();
}
}
else if (this.type == RDebugModel.R_METHOD_BREAKPOINT_TYPE_ID) {
if (this.methodElement != null) {
this.startOffset= this.methodElement.getSourceRange().getStartOffset();
if (this.startOffset < 0) {
setInvalid();
return;
}
this.line= this.document.getLineOfOffset(this.startOffset);
final IRegion lineInformation= this.document.getLineInformation(this.line);
final RHeuristicTokenScanner scanner= RHeuristicTokenScanner.create(
this.sourceUnit.getDocumentContentInfo() );
scanner.configure(this.document, IRDocumentConstants.R_CODE_CONTENT_CONSTRAINT);
this.endOffset= scanner.findNonBlankBackward(
Math.min(lineInformation.getOffset() + lineInformation.getLength(),
this.startOffset + this.methodElement.getSourceRange().getLength() ),
this.startOffset - 1, true );
if (this.endOffset < 0) {
setInvalid();
return;
}
this.baseElement= searchBaseElement(this.methodElement);
if (this.baseElement == null) {
setInvalid();
return;
}
if (this.baseElement != this.methodElement) {
this.astNode= this.methodElement.getAdapter(FDef.class).getContChild();
if (this.astNode == null) {
setInvalid();
return;
}
this.baseExpressionRootNode= this.baseElement.getAdapter(FDef.class).getContChild();
if (!isBaseExpressionRootNodeValid()) {
// new Status(IStatus.ERROR, RDebugCorePlugin.BUNDLE_ID, "Only in blocks.");
setInvalid();
return;
}
}
}
else {
setInvalid();
return;
}
}
else {
throw new IllegalStateException(this.type);
}
}
catch (final BadLocationException e) {
setInvalid();
}
}
private boolean isBaseExpressionRootNodeValid() {
return (this.baseExpressionRootNode != null
&& this.baseExpressionRootNode.getNodeType() == NodeType.BLOCK
&& RAst.isParentChild(this.baseExpressionRootNode, this.astNode) );
}
private IRModelInfo getModelInfo(final IProgressMonitor monitor) {
if (this.modelInfo == null) {
this.modelInfo= (IRModelInfo) this.sourceUnit.getModelInfo(RModel.R_TYPE_ID,
IModelManager.MODEL_FILE, monitor );
}
return this.modelInfo;
}
private void setInvalid() {
this.type= null;
}
private IRLangSourceElement searchMethodElement(final int offset, final IProgressMonitor monitor)
throws BadLocationException {
final IRModelInfo modelInfo= getModelInfo(monitor);
if (modelInfo == null) {
return null;
}
final IRegion lineInformation= this.document.getLineInformationOfOffset(offset);
final RHeuristicTokenScanner scanner= RHeuristicTokenScanner.create(
this.sourceUnit.getDocumentContentInfo() );
scanner.configure(this.document, IRDocumentConstants.R_CODE_CONTENT_CONSTRAINT);
int charStart= scanner.findNonBlankForward(
lineInformation.getOffset(),
lineInformation.getOffset() + lineInformation.getLength(),
true);
if (charStart < 0) {
charStart= offset;
}
ISourceStructElement element= LTKUtils.getCoveringSourceElement(
modelInfo.getSourceElement(), charStart, charStart );
while (element != null) {
if (element instanceof IRLangSourceElement
&& (element.getElementType() & IModelElement.MASK_C1) == IModelElement.C1_METHOD) {
return (IRLangSourceElement) element;
}
element= element.getSourceParent();
}
return null;
}
private RAstNode searchSuspendAstNode(final int offset, final IProgressMonitor monitor) {
final IRModelInfo modelInfo= getModelInfo(monitor);
if (modelInfo == null) {
return null;
}
final AstNode astNode= AstSelection.search(modelInfo.getAst().getRoot(),
offset, offset, AstSelection.MODE_COVERING_SAME_FIRST).getCovering();
if (astNode instanceof RAstNode) {
RAstNode rNode= (RAstNode) astNode;
if (rNode.getStartOffset() < offset) {
final AtomicReference<RAstNode> ref= new AtomicReference<>();
try {
rNode.acceptInR(new GenericVisitor() {
@Override
public void visitNode(final RAstNode node) throws InvocationTargetException {
if (ref.get() != null) {
return;
}
if (node.getStartOffset() >= offset) {
ref.set(node);
return;
}
if (node.getEndOffset() >= offset) {
node.acceptInRChildren(this);
}
}
});
}
catch (final InvocationTargetException e) {}
if (ref.get() != null) {
return ref.get();
}
}
else {
RAstNode rParent;
while ((rParent= rNode.getRParent()) != null && rParent.getStartOffset() >= offset) {
rNode= rParent;
}
}
return rNode;
}
return null;
}
private IRLangSourceElement searchBaseElement(IRLangSourceElement element) {
while (element != null) {
final ISourceStructElement parent= element.getSourceParent();
if (!(parent instanceof IRLangSourceElement)) {
return null;
}
if ((parent.getElementType() & IModelElement.MASK_C1) == IModelElement.C1_SOURCE) {
if ((element.getElementType() & IModelElement.MASK_C1) == IModelElement.C1_METHOD
&& element.getAdapter(FDef.class) != null) {
return element;
}
return null;
}
element= (IRLangSourceElement) parent;
}
// while (element != null) {
// IRFrame frame= (IRFrame) element.getAdapter(IRFrame.class);
// if (frame == null) {
// return null;
// }
// switch (frame.getFrameType()) {
// case IRFrame.FUNCTION:
// element= element.getSourceParent();
// continue;
// case IRFrame.PROJECT:
// case IRFrame.PACKAGE:
// return element;
// default:
// return null;
// }
// }
return null;
}
public String getType() {
return this.type;
}
/**
* Returns the line number of the original specified offset.
*
* @return the line number (1-based)
*/
public int getOriginalLineNumber() {
return (this.originalLine + 1);
}
/**
* Returns the line number of the found breakpoint position.
*
* @return the line number (1-based)
*/
public int getLineNumber() {
return (this.line >= 0) ? (this.line + 1) : -1;
}
/**
* Returns the offset of the start of the breakpoint region.
*
* @return start offset in the document
*/
public int getCharStart() {
return this.startOffset;
}
/**
* Returns the offset of the end of the breakpoint region.
*
* @return end offset in the document
*/
public int getCharEnd() {
return (this.endOffset >= 0) ? (this.endOffset + 1) : -1;
}
public ISourceStructElement getMethodElement() {
return this.methodElement;
}
public ISourceStructElement getBaseElement() {
return this.baseElement;
}
public RAstNode getAstNode() {
return this.astNode;
}
public int computeElementType() throws CoreException {
if (this.type == null) {
throw invalid();
}
if (this.baseElement != null) {
if ((this.baseElement.getElementType() & IModelElement.MASK_C1) == IModelElement.C1_METHOD) {
if (this.baseElement.getElementType() == IRElement.R_S4METHOD) {
return IRLineBreakpoint.R_S4_METHOD_ELEMENT_TYPE;
}
return IRLineBreakpoint.R_COMMON_FUNCTION_ELEMENT_TYPE;
}
}
else { // script line
return IRLineBreakpoint.R_TOPLEVEL_COMMAND_ELEMENT_TYPE;
}
return -1;
}
public String computeElementId() throws CoreException {
if (this.type == null) {
throw invalid();
}
if (this.baseElement != null) {
return RDbg.getElementId(this.baseElement);
}
else { // script line
return TOPLEVEL_ELEMENT_ID;
}
}
public String computeElementLabel() throws CoreException {
if (this.type == null) {
throw invalid();
}
if (this.baseElement != null) {
return getLabel(this.baseElement);
}
else { // script line
try {
return this.document.get(getCharStart(), getCharEnd()-getCharStart());
}
catch (final BadLocationException e) {
return null;
}
}
}
public String computeSubLabel() throws CoreException {
if (this.type == null) {
throw invalid();
}
if (this.baseElement != null) {
RAstNode astNode= this.astNode;
while (astNode != null && astNode.getNodeType() != NodeType.F_DEF) {
astNode= astNode.getRParent();
}
if (astNode != null && (this.methodElement == null
|| (astNode != this.methodElement.getAdapter(FDef.class)
&& astNode.getStartOffset() > this.methodElement.getSourceRange().getStartOffset() ))) {
return "<unnamed>";
}
else if (this.methodElement != null && this.methodElement != this.baseElement) {
return getLabel(this.methodElement);
}
else {
return null;
}
}
else { // script line
return null;
}
}
private String getLabel(final ISourceStructElement element) {
return element.getElementName().toString();
}
public @Nullable IRSrcref computeElementSrcref() throws CoreException {
if (this.type == null) {
throw invalid();
}
try {
if (this.baseElement != null) {
final FDef astNode= this.baseElement.getAdapter(FDef.class);
if (astNode != null) {
return new RSrcref(this.document, astNode.getContChild());
}
}
else {
if (this.baseExpressionRootNode != null) {
return new RSrcref(this.document, this.baseExpressionRootNode);
}
}
return null;
}
catch (final BadLocationException e) {
throw failedComputing(e);
}
}
public int[] computeRExpressionIndex() throws CoreException {
if (this.type == null) {
throw invalid();
}
if (this.astNode != null && this.baseExpressionRootNode != null) {
return RAst.computeRExpressionIndex(this.astNode, this.baseExpressionRootNode);
}
else {
return null;
}
}
public RSrcref @Nullable [] computeRExpressionSrcrefs() throws CoreException {
if (this.type == null) {
throw invalid();
}
if (this.astNode != null && this.baseExpressionRootNode != null) {
final List<RAstNode> nodes= RAst.computeRExpressionNodes(this.astNode, this.baseExpressionRootNode);
try {
final RSrcref[] srcrefs= new RSrcref[nodes.size()];
for (int i= 0; i < srcrefs.length; i++) {
final RAstNode node= nodes.get(i);
if (i == srcrefs.length - 1 || node.getNodeType() == NodeType.BLOCK) {
srcrefs[i]= new RSrcref(this.document, node);
}
}
return srcrefs;
}
catch (final BadLocationException e) {
throw failedComputing(e);
}
}
else {
return null;
}
}
/**
* Creates a breakpoint with the found specifications.
*
* @param monitor
*/
public IRBreakpoint createBreakpoint(final IProgressMonitor monitor) {
if (this.type == null) {
// new Status(IStatus.ERROR, RDebugCorePlugin.BUNDLE_ID, "No valid breakpoint position.");
return null;
}
else if (this.type == RDebugModel.R_LINE_BREAKPOINT_TYPE_ID) {
try {
final String elementId= computeElementId();
final RLineBreakpoint internal= new RLineBreakpoint(this.sourceUnit.getResource(),
getLineNumber(), getCharStart(), getCharEnd(),
computeElementType(), elementId, computeElementLabel(), computeSubLabel(),
false );
internal.setCachedData(new CachedData(
this.modelInfo.getStamp().getSourceStamp(), elementId,
computeRExpressionIndex() ));
return internal;
}
catch (final Exception e) {
RDebugCorePlugin.log(new Status(IStatus.ERROR, RDebugCorePlugin.BUNDLE_ID,
"An error occurred when creating R line breakpoint from validation data\n" + toString(),
e ));
// new Status(IStatus.ERROR, RDebugCorePlugin.BUNDLE_ID,
// "Creating R line breakpoint failed.");
return null;
}
}
else if (this.type == RDebugModel.R_METHOD_BREAKPOINT_TYPE_ID) {
try {
final String elementId= computeElementId();
final RMethodBreakpoint internal= new RMethodBreakpoint(this.sourceUnit.getResource(),
getLineNumber(), getCharStart(), getCharEnd(),
computeElementType(), elementId, computeElementLabel(), computeSubLabel(),
false );
internal.setCachedData(new CachedData(
this.modelInfo.getStamp().getSourceStamp(), elementId,
computeRExpressionIndex() ));
return internal;
}
catch (final Exception e) {
RDebugCorePlugin.log(new Status(IStatus.ERROR, RDebugCorePlugin.BUNDLE_ID,
"An error occurred when creating R method breakpoint from validation data\n" + toString(),
e ));
// new Status(IStatus.ERROR, RDebugCorePlugin.BUNDLE_ID,
// "Create R method breakpoint failed.");
return null;
}
}
throw new IllegalStateException("type= " + this.type);
}
public void updateBreakpoint(final IRBreakpoint breakpoint) throws CoreException {
if (this.type != breakpoint.getBreakpointType()) {
throw new IllegalArgumentException(this.type);
}
if (!(breakpoint instanceof IRLineBreakpoint)) {
throw new IllegalArgumentException(breakpoint.getClass().getName());
}
final IMarker marker= breakpoint.getMarker();
final String elementId= computeElementId();
RGenericLineBreakpoint.updatePosition(marker,
getLineNumber(), getCharStart(), getCharEnd() );
RGenericLineBreakpoint.updateElementInfo(marker,
computeElementType(), elementId, computeElementLabel(), computeSubLabel() );
if (breakpoint instanceof RGenericLineBreakpoint) {
((RGenericLineBreakpoint) breakpoint).setCachedData(new CachedData(
this.modelInfo.getStamp().getSourceStamp(), elementId,
computeRExpressionIndex() ));
}
}
private CoreException invalid() {
return new CoreException(new Status(IStatus.ERROR, RDebugCorePlugin.BUNDLE_ID, 0,
"Validation result was negative.", null ));
}
private CoreException failedComputing(final Throwable e) {
return new CoreException(new Status(IStatus.ERROR, RDebugCorePlugin.BUNDLE_ID, 0,
"An error occurred when computing breakpoint data.", e ));
}
@Override
public String toString() {
final StringBuilder sb= new StringBuilder(getClass().getName());
sb.append("\n").append("validator result:");
sb.append("\n\t").append("type= ").append((this.type != null) ? this.type : "<no valid position found>");
sb.append("\n\t").append("lineNumber= ").append(getLineNumber());
sb.append("\n\t").append("charStart= ").append(getCharStart());
sb.append("\n\t").append("charEnd= ").append(getCharEnd());
return sb.toString();
}
}