Bug 577760: Add AbstractAstProblemReporter

Change-Id: I107c70e76b023c1c37f00caacddba517cb7c4e78
diff --git a/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/ast/core/util/AbstractAstProblemReporter.java b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/ast/core/util/AbstractAstProblemReporter.java
new file mode 100644
index 0000000..a68844d
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/ast/core/util/AbstractAstProblemReporter.java
@@ -0,0 +1,197 @@
+/*=============================================================================#
+ # Copyright (c) 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.ltk.ast.core.util;
+
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullLateInit;
+
+import static org.eclipse.statet.ltk.core.StatusCodes.CTX12;
+import static org.eclipse.statet.ltk.core.StatusCodes.SUBSEQUENT;
+import static org.eclipse.statet.ltk.core.StatusCodes.TYPE1;
+import static org.eclipse.statet.ltk.core.StatusCodes.TYPE123;
+import static org.eclipse.statet.ltk.core.StatusCodes.TYPE1_RUNTIME_ERROR;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.text.BadLocationException;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.text.core.TextLineInformation;
+
+import org.eclipse.statet.ecommons.runtime.core.util.MessageBuilder;
+
+import org.eclipse.statet.internal.ltk.core.LtkCorePlugin;
+import org.eclipse.statet.ltk.ast.core.AstNode;
+import org.eclipse.statet.ltk.core.LtkCore;
+import org.eclipse.statet.ltk.core.StatusCodes;
+import org.eclipse.statet.ltk.core.source.SourceContent;
+import org.eclipse.statet.ltk.core.util.SourceMessageUtil;
+import org.eclipse.statet.ltk.issues.core.IssueRequestor;
+import org.eclipse.statet.ltk.issues.core.Problem;
+import org.eclipse.statet.ltk.issues.core.impl.BasicProblem;
+
+
+@NonNullByDefault
+public class AbstractAstProblemReporter {
+	
+	
+	protected static final int MASK= TYPE123 | CTX12 | SUBSEQUENT;
+	
+	
+	private final String modelTypeId;
+	
+	private final boolean reportSubsequent= false;
+	
+	private SourceContent sourceContent= nonNullLateInit();
+	private IssueRequestor requestor= nonNullLateInit();
+	
+	private final SourceMessageUtil messageUtil= new SourceMessageUtil();
+	private final MessageBuilder messageBuilder= new MessageBuilder();
+	private final List<Problem> problemBuffer= new ArrayList<>(100);
+	
+	
+	public AbstractAstProblemReporter(final String modelTypeId) {
+		this.modelTypeId= modelTypeId;
+	}
+	
+	
+	public String getModelTypeId() {
+		return this.modelTypeId;
+	}
+	
+	
+	protected void init(final SourceContent content, final IssueRequestor requestor) {
+		this.sourceContent= nonNullAssert(content);
+		this.messageUtil.setSourceContent(content);
+		this.requestor= nonNullAssert(requestor);
+	}
+	
+	protected void flush() {
+		if (!this.problemBuffer.isEmpty()) {
+			this.requestor.acceptProblems(this.modelTypeId, this.problemBuffer);
+		}
+	}
+	
+	@SuppressWarnings("null")
+	protected void clear() {
+		this.problemBuffer.clear();
+		this.sourceContent= null;
+		this.requestor= null;
+	}
+	
+	
+	protected final boolean requiredCheck(final int code) {
+		return code != StatusCodes.TYPE1_OK &&
+				(this.reportSubsequent || (code & SUBSEQUENT) == 0);
+	}
+	
+	protected final SourceMessageUtil getMessageUtil() {
+		return this.messageUtil;
+	}
+	
+	protected final MessageBuilder getMessageBuilder() {
+		return this.messageBuilder;
+	}
+	
+	protected final void addProblem(final int severity, final int code, final String message,
+			int startOffset, int endOffset) {
+		if (startOffset < this.sourceContent.getStartOffset()) {
+			startOffset= this.sourceContent.getStartOffset();
+		}
+		if (endOffset < startOffset) {
+			endOffset= startOffset;
+		}
+		else if (endOffset > this.sourceContent.getEndOffset()) {
+			endOffset= this.sourceContent.getEndOffset();
+		}
+		
+		this.problemBuffer.add(new BasicProblem(this.modelTypeId, severity, code, message,
+				startOffset, endOffset ));
+		
+		if (this.problemBuffer.size() >= 100) {
+			this.requestor.acceptProblems(this.modelTypeId, this.problemBuffer);
+			this.problemBuffer.clear();
+		}
+	}
+	
+	
+	protected void handleCommonCodes(final AstNode node, final int code)
+			throws BadLocationException, InvocationTargetException {
+		switch (code & TYPE1) {
+		
+		case TYPE1_RUNTIME_ERROR:
+			addProblem(Problem.SEVERITY_ERROR, code,
+					"Error when parsing source code. Please submit a bug report with a code snippet / log entry.", //$NON-NLS-1$
+					node.getStartOffset(), node.getStartOffset() );
+			return;
+		
+		default:
+			handleUnknownCodes(node);
+			return;
+		}
+	}
+	
+	protected void handleUnknownCodes(final AstNode node) {
+		final int code= (node.getStatusCode() & MASK);
+		final StringBuilder sb= new StringBuilder();
+		sb.append("Unhandled/Unknown code of AST node (").append(this.modelTypeId).append("):"); //$NON-NLS-1$ //$NON-NLS-2$
+		sb.append('\n');
+		sb.append("  code= 0x").append(Integer.toHexString(code)); //$NON-NLS-1$
+		sb.append('\n');
+		sb.append("  node= ").append(node);
+		sb.append(" (").append(node.getStartOffset()).append(", ").append(node.getLength()).append(')'); //$NON-NLS-1$ //$NON-NLS-2$
+		sb.append('\n');
+		if (this.sourceContent != null) {
+			final TextLineInformation lines= this.sourceContent.getStringLines();
+			final int line= lines.getLineOfOffset(node.getStartOffset() - this.sourceContent.getStartOffset());
+			sb.append("  Line ").append((line + 1)); //$NON-NLS-1$
+			sb.append('\n');
+			
+			final int firstLine= Math.max(0, line - 2);
+			final int lastLine= Math.min(lines.getNumberOfLines() - 1,
+					lines.getLineOfOffset(node.getEndOffset() - this.sourceContent.getStartOffset()) + 2 );
+			sb.append("  source (line ").append(firstLine + 1).append('-').append(lastLine + 1).append(")= \n"); //$NON-NLS-1$ //$NON-NLS-2$
+			sb.append(this.sourceContent.getString(),
+					lines.getStartOffset(firstLine), lines.getEndOffset(lastLine) );
+		}
+		LtkCorePlugin.log(new Status(IStatus.WARNING, LtkCore.BUNDLE_ID, sb.toString()));
+	}
+	
+	
+	protected int expandSpaceStart(final int offset) {
+		switch ((this.sourceContent.contains(offset - 1)) ? this.sourceContent.getChar(offset - 1) : -1) {
+		case ' ':
+		case '\t':
+			return offset - 1;
+		default:
+			return offset;
+		}
+	}
+	
+	protected int expandSpaceEnd(final int offset) {
+		switch ((this.sourceContent.contains(offset)) ? this.sourceContent.getChar(offset) : -1) {
+		case ' ':
+		case '\t':
+			return offset + 1;
+		default:
+			return offset;
+		}
+	}
+	
+}