Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor out preproc.issue #96

Merged
merged 4 commits into from
Apr 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion java/src/processing/mode/java/ProblemFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import processing.app.Problem;
import processing.app.ui.Editor;
import processing.mode.java.preproc.issue.PdePreprocessIssue;
import processing.mode.java.preproc.PdePreprocessIssue;


/**
Expand Down
357 changes: 357 additions & 0 deletions java/src/processing/mode/java/preproc/PdeIssueEmitter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
Part of the Processing project - http://processing.org

Copyright (c) 2019 The Processing Foundation

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/

package processing.mode.java.preproc;

import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import processing.mode.java.SourceUtil;

import java.util.Optional;


/**
* ANTLR error listener to inform a preprocess issue listener when syntax errors are encountered.
*
* <p>
* A {BaseErrorListener} which looks for syntax errors reported by ANTLR and converts them to
* {PdePreprocessIssue}s that are consumable by a {PdePreprocessIssueListener}. It does this by
* running the {PreprocessIssueMessageSimplifier} to generate a more user-friendly error message
* before informing the provided listener.
* </p>
*/
public class PdeIssueEmitter extends BaseErrorListener {

private final PdePreprocessIssueListener listener;
private final Optional<SourceEmitter> sourceMaybe;

/**
* Create a new issue emitter.
*
* <p>
* Create a new issue emitter when access to the processing sketch source is not available.
* Note that this will not allow some error beautification and, if sketch source is available,
* use other constructor.
* </p>
*
* @param newListener The listener to inform when encountering a syntax error.
*/
public PdeIssueEmitter(PdePreprocessIssueListener newListener) {
listener = newListener;
sourceMaybe = Optional.empty();
}

/**
* Create a new issue emitter.
*
* @param newListener The listener to inform when encountering a syntax error.
* @param newSourceEmitter The sketch source to use when helping beautify certain syntax error
* messages.
*/
public PdeIssueEmitter(PdePreprocessIssueListener newListener, SourceEmitter newSourceEmitter) {
listener = newListener;
sourceMaybe = Optional.of(newSourceEmitter);
}

@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line,
int charPositionInLine, String msg, RecognitionException e) {

PreprocessIssueMessageSimplifier facade = PreprocessIssueMessageSimplifier.get();
IssueMessageSimplification simplification = facade.simplify(msg);

IssueLocation issueLocation;

if (sourceMaybe.isPresent()) {
issueLocation = IssueLocationFactory.getLineWithOffset(
simplification,
line,
charPositionInLine,
sourceMaybe.get().getSource()
);
} else {
issueLocation = new IssueLocation(line, charPositionInLine);
}

listener.onIssue(new PdePreprocessIssue(
issueLocation.getLine(),
issueLocation.getCharPosition(),
simplification.getMessage()
));
}

/**
* Simple interface for strategy which can emit the full body of a processing sketch.
*/
public static interface SourceEmitter {

/**
* Get the full body of the processing sketch.
*
* @return String processing sketch source code across all tabs.
*/
String getSource();

}

/**
* Interface for listener that responds to issues reported by the preprocessor.
*/
public static interface PdePreprocessIssueListener {

/**
* Callback to invoke when an issue is encountered in preprocesing.
*
* @param issue Description of the issue.
*/
void onIssue(PdePreprocessIssue issue);

}

/**
* ================================
* == Supporting data structures ==
* ================================
*/

/**
* Data structure describing an issue simplification or explanation.
*
* <p>
* Data structure describing an edit that was made to an error message or warning to be shown to
* the user based on a series of rules that attempts to make error messages easier to understand
* for the user.
* </p>
*/
public static class IssueMessageSimplification {

private final String message;
private final boolean attributeToPriorToken;

/**
* Create a new issue message simplification.
*
* <p>
* Create a new issue message simplification that leaves the token attribution alone (the token
* on which the error was reported will be the same before error message simplification).
* </p>
*
* @param newMessage The message to show to the user.
*/
public IssueMessageSimplification(String newMessage) {
message = newMessage;
attributeToPriorToken = false;
}

/**
* Create a new issue message simplification.
*
* <p>
* Create a new issue message simplification. Note that there is an option to have the error
* attributed to the "prior token". This is helpful, for example, when a semicolon is missing.
* The error is generated on the token after the line on which the semicolon was omitted so,
* while the error technically emerges on the next line, it is better for the user for it to
* appear earlier. Specifically, it is most sensible for it to appear on the "prior token".
* </p>
*
* @param newMessage The message to show to the user.
* @param newAttributeToPriorToken Boolean flag indicating if the error should be shown on the
* token prior to the one on which the error was originally generated. True if the error should
* be attributed to the prior token. False otherwise.
*/
public IssueMessageSimplification(String newMessage, boolean newAttributeToPriorToken) {
message = newMessage;
attributeToPriorToken = newAttributeToPriorToken;
}

/**
* Get the error message text that should be shown to the user.
*
* @return The error message text that should be shown to the user.
*/
public String getMessage() {
return message;
}

/**
* Flag indicating if the error should be attributed to the prior token.
*
* @return True if the error should be attributed to the prior non-skip token (not whitepsace or
* comment). This is useful when a mistake on a prior line like omitted semicolon causes an
* error on a later line but one wants error highlighting closer to the mistake itself. False
* if the error should be attributed to the original offending token.
*/
public boolean getAttributeToPriorToken() {
return attributeToPriorToken;
}

}

/**
* Data structure describing where an issue occurred.
*/
public static class IssueLocation {

private final int line;
private final int charPosition;

/**
* Create a new issue location structure.
*
* @param newLine The line (1-indexed) where the issue occurred. This should be in the global file
* generated by the preprocessor and not relative to the start of the tab.
* @param newCharPosition The position on the line.
*/
public IssueLocation(int newLine, int newCharPosition) {
line = newLine;
charPosition = newCharPosition;
}

/**
* Get the 1-indexed line on which this error occurred.
*
* @return The line on which this error occurred. Note that this will be relative to the global
* file generated by the preprocessor and not relative to the start of the tab.
*/
public int getLine() {
return line;
}

/**
* The the position of the error within the line.
*
* @return The number of characters including whitespace from the start of the line at which the
* error occurred.
*/
public int getCharPosition() {
return charPosition;
}

}

/**
* =====================
* == Utility classes ==
* =====================
*/

/**
* Utility that can help clean up where in source an issue should be reported.
*
* <p>
* For some errors, the location of the "mistake" does not appear close to where the actual error
* is generated. For example, consider omitting a semicolon. Though the "mistake" is arguably on
* the line on which a semicolon is forgotten, the grammatical error appears in the first
* non-skip token after the omitted character. This means that the issue shown to the user may
* be far away from the line they would want to edit. This utility helps determine if an issue
* requires a new location and, if so, where the location should be.
* </p>
*/
public static class IssueLocationFactory {

/**
* Determine where an issue should be reported.
*
* @param simplification The issue simplification generated from {PreprocessIssueMessageSimplifier}.
* @param originalLine The original line (1 indexed) on which the issue was reported.
* @param originalOffset The original number of characters from the start of the line where the
* the issue was reported.
* @param source The full concatenated source of the sketch being built.
* @param lineCount The total
* @return The new location where the issue should be reported. This may be identical to the
* original location if the issue was not moved.
*/
public static IssueLocation getLineWithOffset(IssueMessageSimplification simplification,
int originalLine, int originalOffset, String source) {

// Determine if the issue should be relocated
boolean shouldAttributeToPrior = simplification.getAttributeToPriorToken();
shouldAttributeToPrior = shouldAttributeToPrior && originalLine != 0;

if (!shouldAttributeToPrior) {
return new IssueLocation(originalLine, originalOffset);
}

// Find the code prior the issue
String priorCode = getContentsUpToLine(source, originalLine);

// Find the token immediately prior to the issue
PreprocessIssueMessageSimplifier.PriorTokenFinder finder = new PreprocessIssueMessageSimplifier.PriorTokenFinder();
int charPos = priorCode.length();
while (!finder.isDone() && charPos > 0) {
charPos--;
finder.step(priorCode.charAt(charPos));
}

// Find the location offset depending on if the prior token could be found
Optional<Integer> foundStartOfMatchMaybe = finder.getTokenPositionMaybe();
int startOfMatch;
int linesOffset;

if (foundStartOfMatchMaybe.isPresent()) {
startOfMatch = priorCode.length() - foundStartOfMatchMaybe.get();
String contentsOfMatch = priorCode.substring(startOfMatch);
linesOffset = SourceUtil.getCount(contentsOfMatch, "\n");
} else {
startOfMatch = priorCode.length();
linesOffset = 0;
}

// Apply the location offset and highlight to the end of the line
String contentsPriorToMatch = priorCode.substring(0, startOfMatch);
int newLine = originalLine - linesOffset;
int lengthIncludingLine = contentsPriorToMatch.length();
int lengthExcludingLine = contentsPriorToMatch.lastIndexOf('\n');
int lineLength = lengthIncludingLine - lengthExcludingLine;
int col = lineLength - 1; // highlight from start of line to end

// Build the new issue location
return new IssueLocation(newLine, col);
}

/**
* Get all of the contents of source leading up to a line.
*
* @param source The full concatenated sketch source.
* @param endLineExclusive The line up to which code should be returned. Note that this is an
* "exclusive" boundary. Code from this line itself will not be included.
* @return All of the sketch code leading up to but not including the line given.
*/
private static String getContentsUpToLine(String source, int endLineExclusive) {
int line = 0;
int stringCursor = 0;
int strLength = source.length();

while (line < endLineExclusive-1 && stringCursor < strLength) {
if (source.charAt(stringCursor) == '\n') {
line++;
}

stringCursor++;
}

return source.substring(0, stringCursor);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@
import processing.mode.java.SourceUtil;
import processing.mode.java.TextTransform;
import processing.mode.java.preproc.PdePreprocessor.Mode;
import processing.mode.java.preproc.issue.PdePreprocessIssue;
import processing.mode.java.preproc.issue.PreprocessIssueMessageSimplifier;

/**
* ANTLR tree traversal listener that preforms code rewrites as part of sketch preprocessing.
Expand Down
Loading