diff --git a/app/build.xml b/app/build.xml
index b6d97218ab..2bf8c74661 100644
--- a/app/build.xml
+++ b/app/build.xml
@@ -1,55 +1,55 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/app/src/processing/app/Problem.java b/app/src/processing/app/Problem.java
index cb12ad5e3e..0c5e08c26a 100644
--- a/app/src/processing/app/Problem.java
+++ b/app/src/processing/app/Problem.java
@@ -31,5 +31,8 @@ public interface Problem {
public int getStartOffset();
public int getStopOffset();
+
+ public String getMatchingRefURL();
+ public void setMatchingRefURL(String url);
}
diff --git a/app/src/processing/app/ui/Editor.java b/app/src/processing/app/ui/Editor.java
index 6780a53100..c53527ccc3 100644
--- a/app/src/processing/app/ui/Editor.java
+++ b/app/src/processing/app/ui/Editor.java
@@ -443,7 +443,6 @@ public void addErrorTable(EditorFooter ef) {
ef.addPanel(scrollPane, Language.text("editor.footer.errors"), "/lib/footer/error");
}
-
public EditorState getEditorState() {
return state;
}
@@ -598,8 +597,6 @@ public EditorConsole getConsole() {
return console;
}
-
-
// public Settings getTheme() {
// return mode.getTheme();
// }
@@ -3064,7 +3061,6 @@ public void setProblemList(List problems) {
updateEditorStatus();
}
-
/**
* Updates the error table in the Error Window.
*/
@@ -3082,7 +3078,6 @@ public void updateErrorTable(List problems) {
}
}
-
public void highlight(Problem p) {
if (p != null) {
highlight(p.getTabIndex(), p.getStartOffset(), p.getStopOffset());
@@ -3118,7 +3113,7 @@ public List getProblems() {
* Updates editor status bar, depending on whether the caret is on an error
* line or not
*/
- public void updateEditorStatus() {
+ public Problem updateEditorStatus() {
Problem problem = findProblem(textarea.getCaretLine());
if (problem != null) {
int type = problem.isError() ?
@@ -3132,6 +3127,8 @@ public void updateEditorStatus() {
break;
}
}
+
+ return problem;
}
diff --git a/app/src/processing/app/ui/EditorHints.java b/app/src/processing/app/ui/EditorHints.java
new file mode 100644
index 0000000000..d77572f733
--- /dev/null
+++ b/app/src/processing/app/ui/EditorHints.java
@@ -0,0 +1,169 @@
+package processing.app.ui;
+
+import javax.swing.*;
+import javax.swing.border.Border;
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.List;
+
+public class EditorHints extends JPanel {
+ private static final Border EMPTY_SPACING = BorderFactory.createEmptyBorder(
+ 10, 10, 10, 10
+ );
+ private static final Border GREEN_BORDER = BorderFactory.createLineBorder(
+ new Color(71, 151, 97), 2
+ );
+ private static final Border RED_BORDER = BorderFactory.createLineBorder(
+ new Color(232, 90, 79), 2
+ );
+
+ private final List HINTS;
+
+ private final JScrollPane SCROLL_PANE;
+ private final JLabel PROBLEM_TITLE_LABEL;
+ private final JLabel SUGGESTION_TITLE_LABEL;
+ private final JLabel SUGGESTION_COUNTER;
+ private final Box BAD_CODE_BOX;
+ private final Box GOOD_CODE_BOX;
+ private final Box NAV_BOX;
+
+ private int hintIndex;
+
+ public EditorHints(JScrollPane scrollPane) {
+ HINTS = new ArrayList<>();
+
+ SCROLL_PANE = scrollPane;
+
+ setLayout(new BorderLayout());
+ setBorder(EMPTY_SPACING);
+
+ // Create title labels
+ PROBLEM_TITLE_LABEL = new JLabel();
+ SUGGESTION_TITLE_LABEL = new JLabel();
+
+ Font probFont = PROBLEM_TITLE_LABEL.getFont();
+ Font boldFont = probFont.deriveFont(probFont.getStyle() ^ Font.BOLD);
+ PROBLEM_TITLE_LABEL.setFont(boldFont);
+
+ Box titleBox = Box.createVerticalBox();
+ titleBox.add(PROBLEM_TITLE_LABEL);
+ titleBox.add(SUGGESTION_TITLE_LABEL);
+
+ // Create suggestion counter
+ SUGGESTION_COUNTER = new JLabel();
+
+ // Add header layout
+ Box headerBox = Box.createHorizontalBox();
+ headerBox.add(titleBox);
+ headerBox.add(Box.createHorizontalGlue());
+ headerBox.add(SUGGESTION_COUNTER);
+
+ // Create a split box to hold code examples
+ Box codeBox = Box.createHorizontalBox();
+ BAD_CODE_BOX = Box.createVerticalBox();
+ GOOD_CODE_BOX = Box.createVerticalBox();
+
+ BAD_CODE_BOX.setBorder(EMPTY_SPACING);
+ GOOD_CODE_BOX.setBorder(EMPTY_SPACING);
+
+ codeBox.add(BAD_CODE_BOX);
+ codeBox.add(GOOD_CODE_BOX);
+
+ // Create navigation button
+ JButton navButton = new JButton("View Next Hint");
+ navButton.setFocusable(false); // Stop the button from glowing on press
+ navButton.addActionListener(
+ (event) -> setVisibleHint((hintIndex + 1) % HINTS.size())
+ );
+
+ NAV_BOX = Box.createHorizontalBox();
+ NAV_BOX.add(Box.createHorizontalGlue());
+ NAV_BOX.add(navButton);
+
+ add(headerBox, BorderLayout.NORTH);
+ add(codeBox, BorderLayout.CENTER);
+ add(NAV_BOX, BorderLayout.SOUTH);
+
+ clear();
+ }
+
+ public void clear() {
+ HINTS.clear();
+
+ PROBLEM_TITLE_LABEL.setVisible(false);
+ SUGGESTION_TITLE_LABEL.setVisible(false);
+ SUGGESTION_COUNTER.setVisible(false);
+ BAD_CODE_BOX.removeAll();
+ GOOD_CODE_BOX.removeAll();
+ NAV_BOX.setVisible(false);
+ }
+
+ public void setCurrentHints(List newHints) {
+ clear();
+
+ if (newHints.size() > 0) {
+ PROBLEM_TITLE_LABEL.setVisible(true);
+ SUGGESTION_TITLE_LABEL.setVisible(true);
+ SUGGESTION_COUNTER.setVisible(true);
+ NAV_BOX.setVisible(true);
+
+ HINTS.addAll(newHints);
+ setVisibleHint(0);
+ }
+ }
+
+ private void setVisibleHint(int index) {
+ hintIndex = index;
+ Hint visibleHint = HINTS.get(index);
+
+ PROBLEM_TITLE_LABEL.setText(visibleHint.getProblemText());
+ SUGGESTION_TITLE_LABEL.setText(visibleHint.getSuggestionText());
+ SUGGESTION_COUNTER.setText("Hint " + (hintIndex + 1)
+ + "/" + HINTS.size());
+
+ BAD_CODE_BOX.removeAll();
+ GOOD_CODE_BOX.removeAll();
+
+ BAD_CODE_BOX.add(new JLabel("Incorrect Code"));
+ GOOD_CODE_BOX.add(new JLabel("Correct Code"));
+
+ for (String badCode : visibleHint.getBadCode()) {
+ addCodeBox(badCode, BAD_CODE_BOX, RED_BORDER);
+ }
+
+ for (String goodCode : visibleHint.getGoodCode()) {
+ addCodeBox(goodCode, GOOD_CODE_BOX, GREEN_BORDER);
+ }
+
+ JScrollBar verticalScrollBar = SCROLL_PANE.getVerticalScrollBar();
+ verticalScrollBar.setValue(verticalScrollBar.getMinimum());
+ }
+
+ private void addCodeBox(String example, JComponent parent, Border border) {
+ parent.add(Box.createVerticalStrut(8));
+
+ JTextArea textArea = new JTextArea(example);
+ textArea.setEditable(false);
+ textArea.setBorder(BorderFactory.createCompoundBorder(border, EMPTY_SPACING));
+ textArea.setLineWrap(true);
+ textArea.setWrapStyleWord(true);
+ parent.add(textArea);
+ }
+
+ public interface Hint {
+
+ void addGoodCode(String goodCode);
+
+ void addBadCode(String badCode);
+
+ String getProblemText();
+
+ String getSuggestionText();
+
+ List getBadCode();
+
+ List getGoodCode();
+
+ }
+
+}
diff --git a/app/src/processing/app/ui/EditorStatus.java b/app/src/processing/app/ui/EditorStatus.java
index 8affa8c6a3..008405143f 100644
--- a/app/src/processing/app/ui/EditorStatus.java
+++ b/app/src/processing/app/ui/EditorStatus.java
@@ -150,7 +150,7 @@ public void mousePressed(MouseEvent e) {
Clipboard clipboard = getToolkit().getSystemClipboard();
clipboard.setContents(new StringSelection(message), null);
System.out.println("Copied to the clipboard. " +
- "Use shift-click to search the web instead.");
+ "Use shift-click to search the web instead.");
}
} else if (rolloverState == ROLLOVER_COLLAPSE) {
@@ -195,16 +195,16 @@ void setCollapsed(boolean newState) {
void updateMouse() {
switch (rolloverState) {
- case ROLLOVER_CLIPBOARD:
- case ROLLOVER_URL:
- setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
- break;
- case ROLLOVER_COLLAPSE:
- setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
- break;
- case ROLLOVER_NONE:
- setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
- break;
+ case ROLLOVER_CLIPBOARD:
+ case ROLLOVER_URL:
+ setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+ break;
+ case ROLLOVER_COLLAPSE:
+ setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+ break;
+ case ROLLOVER_NONE:
+ setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+ break;
}
repaint();
}
@@ -225,27 +225,27 @@ public void updateMode() {
urlColor = mode.getColor("status.url.fgcolor");
fgColor = new Color[] {
- mode.getColor("status.notice.fgcolor"),
- mode.getColor("status.error.fgcolor"),
- mode.getColor("status.error.fgcolor"),
- mode.getColor("status.warning.fgcolor"),
- mode.getColor("status.warning.fgcolor")
+ mode.getColor("status.notice.fgcolor"),
+ mode.getColor("status.error.fgcolor"),
+ mode.getColor("status.error.fgcolor"),
+ mode.getColor("status.warning.fgcolor"),
+ mode.getColor("status.warning.fgcolor")
};
bgColor = new Color[] {
- mode.getColor("status.notice.bgcolor"),
- mode.getColor("status.error.bgcolor"),
- mode.getColor("status.error.bgcolor"),
- mode.getColor("status.warning.bgcolor"),
- mode.getColor("status.warning.bgcolor")
+ mode.getColor("status.notice.bgcolor"),
+ mode.getColor("status.error.bgcolor"),
+ mode.getColor("status.error.bgcolor"),
+ mode.getColor("status.warning.bgcolor"),
+ mode.getColor("status.warning.bgcolor")
};
bgImage = new Image[] {
- mode.loadImage("/lib/status/notice.png"),
- mode.loadImage("/lib/status/error.png"),
- mode.loadImage("/lib/status/error.png"),
- mode.loadImage("/lib/status/warning.png"),
- mode.loadImage("/lib/status/warning.png")
+ mode.loadImage("/lib/status/notice.png"),
+ mode.loadImage("/lib/status/error.png"),
+ mode.loadImage("/lib/status/error.png"),
+ mode.loadImage("/lib/status/warning.png"),
+ mode.loadImage("/lib/status/warning.png")
};
font = mode.getFont("status.font");
@@ -362,9 +362,9 @@ public void paint(Graphics screen) {
rolloverState = ROLLOVER_CLIPBOARD;
} else if (url != null && mouseX > LEFT_MARGIN &&
- // calculate right edge of the text for rollovers (otherwise the pane
- // cannot be resized up or down whenever a URL is being displayed)
- mouseX < (LEFT_MARGIN + g.getFontMetrics().stringWidth(message))) {
+ // calculate right edge of the text for rollovers (otherwise the pane
+ // cannot be resized up or down whenever a URL is being displayed)
+ mouseX < (LEFT_MARGIN + g.getFontMetrics().stringWidth(message))) {
rolloverState = ROLLOVER_URL;
}
}
@@ -426,8 +426,8 @@ private void drawButton(Graphics g, String symbol, int pos, boolean highlight) {
g.setColor(fgColor[mode]);
}
g.drawString(symbol,
- left + (buttonSize - g.getFontMetrics().stringWidth(symbol))/2,
- (sizeH + ascent) / 2);
+ left + (buttonSize - g.getFontMetrics().stringWidth(symbol))/2,
+ (sizeH + ascent) / 2);
}
diff --git a/app/src/processing/app/ui/ErrorTable.java b/app/src/processing/app/ui/ErrorTable.java
index 8abfdf77a0..406417eb6e 100644
--- a/app/src/processing/app/ui/ErrorTable.java
+++ b/app/src/processing/app/ui/ErrorTable.java
@@ -127,7 +127,7 @@ public void clearRows() {
dtm.setRowCount(0);
}
-
+ //EDIT AREA
public void addRow(Problem data, String msg, String filename, String line) {
DefaultTableModel dtm = (DefaultTableModel) getModel();
dtm.addRow(new Object[] { data, msg, filename, line });
diff --git a/build/shared/lib/footer/hint-enabled-1x.png b/build/shared/lib/footer/hint-enabled-1x.png
new file mode 100644
index 0000000000..340ec2914f
Binary files /dev/null and b/build/shared/lib/footer/hint-enabled-1x.png differ
diff --git a/build/shared/lib/footer/hint-enabled-2x.png b/build/shared/lib/footer/hint-enabled-2x.png
new file mode 100644
index 0000000000..9f97d2ebd2
Binary files /dev/null and b/build/shared/lib/footer/hint-enabled-2x.png differ
diff --git a/build/shared/lib/footer/hint-selected-1x.png b/build/shared/lib/footer/hint-selected-1x.png
new file mode 100644
index 0000000000..a9e7b18412
Binary files /dev/null and b/build/shared/lib/footer/hint-selected-1x.png differ
diff --git a/build/shared/lib/footer/hint-selected-2x.png b/build/shared/lib/footer/hint-selected-2x.png
new file mode 100644
index 0000000000..73851205f8
Binary files /dev/null and b/build/shared/lib/footer/hint-selected-2x.png differ
diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java
index 6b87398c15..7683c3216b 100644
--- a/java/src/processing/mode/java/JavaEditor.java
+++ b/java/src/processing/mode/java/JavaEditor.java
@@ -9,6 +9,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import javax.swing.*;
import javax.swing.border.*;
@@ -16,6 +17,9 @@
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
+import javafx.embed.swing.JFXPanel;
+import javafx.scene.Scene;
+import javafx.scene.web.WebView;
import processing.core.PApplet;
import processing.data.StringList;
import processing.app.*;
@@ -28,6 +32,7 @@
import processing.mode.java.debug.LineBreakpoint;
import processing.mode.java.debug.LineHighlight;
import processing.mode.java.debug.LineID;
+import processing.mode.java.pdex.MatchingRefURLAssembler;
import processing.mode.java.pdex.PreprocessingService;
import processing.mode.java.pdex.ImportStatement;
import processing.mode.java.pdex.JavaTextArea;
@@ -70,6 +75,8 @@ public class JavaEditor extends Editor {
private boolean hasJavaTabs;
private boolean javaTabWarned;
+ private WebView webView;
+
protected PreprocessingService preprocessingService;
protected PDEX pdex;
@@ -172,11 +179,22 @@ public void rebuild() {
};
}
+ public void addEditorHints(EditorFooter footer) {
+ JFXPanel embedPanel = new JFXPanel();
+
+ javafx.application.Platform.runLater(() -> {
+ webView = new WebView();
+ embedPanel.setScene(new Scene(webView));
+ });
+
+ footer.addPanel(embedPanel, "Hints", "/lib/footer/hint");
+ }
@Override
public EditorFooter createFooter() {
EditorFooter footer = super.createFooter();
addErrorTable(footer);
+ addEditorHints(footer);
return footer;
}
@@ -1282,6 +1300,63 @@ public void sketchChanged() {
}
+ @Override
+ public Problem updateEditorStatus() {
+ Problem currentProblem = super.updateEditorStatus();
+
+ javafx.application.Platform.runLater(() -> {
+ if (webView != null && currentProblem != null) {
+ webView.getEngine().load(currentProblem.getMatchingRefURL());
+ }
+ });
+
+ return currentProblem;
+ }
+
+ public void statusError(Exception err) {
+ super.statusError(err);
+
+ if (!(err instanceof SketchException)) {
+ return;
+ }
+
+ // Get the MatchingRef URL
+ MatchingRefURLAssembler urlAssembler = new MatchingRefURLAssembler(true);
+ SketchException sketchErr = (SketchException) err;
+ String message = err.getMessage();
+ Optional optionalURL = Optional.empty();
+
+ // Not all errors have a line and column
+ int line = Math.max(sketchErr.getCodeLine(), 0);
+ int column = Math.max(sketchErr.getCodeColumn(), 0);
+
+ String textAboveError = textarea.getText(
+ 0,
+ textarea.getLineStartOffset(line) + column
+ );
+ if (message.equals("expecting EOF, found '}'") && textAboveError != null) {
+ optionalURL = urlAssembler.getClosingCurlyBraceURL(textAboveError);
+ } else if (message.startsWith("expecting DOT")) {
+ optionalURL = urlAssembler.getIncorrectVarDeclarationURL(textarea, sketchErr);
+ } else if (message.equals("It looks like you're mixing \"active\" and \"static\" modes.") && textAboveError != null) {
+ optionalURL = urlAssembler.getIncorrectMethodDeclarationURL(textAboveError);
+ } else if (message.startsWith("unexpected token:")) {
+ String token = message.substring(message.indexOf(':') + 1).trim();
+ optionalURL = urlAssembler.getUnexpectedTokenURL(token);
+ }
+
+ // Load the page
+ if (optionalURL.isPresent()) {
+ final String finalURL = optionalURL.get();
+ javafx.application.Platform.runLater(() -> {
+ if (webView != null) {
+ webView.getEngine().load(finalURL);
+ }
+ });
+ }
+
+ }
+
public void statusError(String what) {
super.statusError(what);
// new Exception("deactivating RUN").printStackTrace();
diff --git a/java/src/processing/mode/java/pdex/ErrorChecker.java b/java/src/processing/mode/java/pdex/ErrorChecker.java
index 4c735bc7f9..068b3b1503 100644
--- a/java/src/processing/mode/java/pdex/ErrorChecker.java
+++ b/java/src/processing/mode/java/pdex/ErrorChecker.java
@@ -8,6 +8,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
@@ -24,6 +25,17 @@
import com.google.classpath.ClassPathFactory;
import com.google.classpath.RegExpResourceFilter;
+import org.eclipse.jdt.core.dom.ASTNode;
+import org.eclipse.jdt.core.dom.ArrayAccess;
+import org.eclipse.jdt.core.dom.ArrayCreation;
+import org.eclipse.jdt.core.dom.DoStatement;
+import org.eclipse.jdt.core.dom.EnhancedForStatement;
+import org.eclipse.jdt.core.dom.FieldDeclaration;
+import org.eclipse.jdt.core.dom.ForStatement;
+import org.eclipse.jdt.core.dom.IfStatement;
+import org.eclipse.jdt.core.dom.SwitchStatement;
+import org.eclipse.jdt.core.dom.TryStatement;
+import org.eclipse.jdt.core.dom.WhileStatement;
import processing.app.Language;
import processing.app.Problem;
import processing.mode.java.JavaEditor;
@@ -137,6 +149,7 @@ private void handleSketchProblems(PreprocessedSketch ps) {
if (scheduledUiUpdate != null) {
scheduledUiUpdate.cancel(true);
}
+
// Update UI after a delay. See #2677
long delay = nextUiUpdate - System.currentTimeMillis();
Runnable uiUpdater = () -> {
@@ -156,11 +169,106 @@ static private JavaProblem convertIProblem(IProblem iproblem, PreprocessedSketch
int line = ps.tabOffsetToTabLine(in.tabIndex, in.startTabOffset);
JavaProblem p = JavaProblem.fromIProblem(iproblem, in.tabIndex, line, badCode);
p.setPDEOffsets(in.startTabOffset, in.stopTabOffset);
+
+ Optional matchingRefURL = getMatchingRefURL(iproblem, ps.compilationUnit);
+ matchingRefURL.ifPresent(p::setMatchingRefURL);
return p;
}
return null;
}
+ static private Optional getMatchingRefURL(IProblem compilerError, ASTNode ast) {
+ String[] problemArguments = compilerError.getArguments();
+ ASTNode problemNode = ASTUtils.getASTNodeAt(
+ ast,
+ compilerError.getSourceStart(),
+ compilerError.getSourceEnd()
+ );
+
+ MatchingRefURLAssembler urlAssembler = new MatchingRefURLAssembler(true);
+
+ switch (compilerError.getID()) {
+ case IProblem.MustDefineEitherDimensionExpressionsOrInitializer:
+ return urlAssembler.getArrDimURL(problemNode);
+ case IProblem.IllegalDimension:
+ return urlAssembler.getTwoDimArrURL(problemNode);
+ case IProblem.CannotDefineDimensionExpressionsWithInit:
+ return urlAssembler.getTwoInitializerArrURL(problemNode);
+ case IProblem.UndefinedMethod:
+ return urlAssembler.getMissingMethodURL(problemNode);
+ case IProblem.ParameterMismatch:
+ return urlAssembler.getParamMismatchURL(problemNode);
+ case IProblem.ShouldReturnValue:
+ return urlAssembler.getMissingReturnURL(problemNode);
+ case IProblem.TypeMismatch:
+ case IProblem.ReturnTypeMismatch:
+ String providedType = truncateClass(problemArguments[0]);
+ String requiredType = truncateClass(problemArguments[1]);
+ return urlAssembler.getTypeMismatchURL(providedType, requiredType, problemNode);
+ case IProblem.UndefinedType:
+ return urlAssembler.getMissingTypeURL(problemArguments[0], problemNode);
+ case IProblem.UnresolvedVariable:
+ return urlAssembler.getMissingVarURL(problemArguments[0], problemNode);
+ case IProblem.UninitializedLocalVariable:
+ return urlAssembler.getUninitializedVarURL(problemArguments[0], problemNode);
+ case IProblem.StaticMethodRequested:
+ return urlAssembler.getStaticErrorURL(problemArguments[0], problemArguments[1], problemNode);
+ case IProblem.UndefinedField:
+ case IProblem.UndefinedName:
+ return urlAssembler.getVariableDeclaratorsURL(problemNode);
+ case IProblem.ParsingErrorInsertToComplete:
+ List argsList = Arrays.asList(problemArguments);
+
+ // Handle incorrect variable declaration
+ if (argsList.contains("VariableDeclarators")) {
+ return urlAssembler.getVariableDeclaratorsURL(problemNode);
+ }
+
+ ASTNode parent = problemNode.getParent();
+ ASTNode grandparent = problemNode.getParent().getParent();
+ if (parent instanceof ArrayCreation || grandparent instanceof ArrayAccess || argsList.contains("Dimensions")
+ || (parent instanceof FieldDeclaration && ((FieldDeclaration) parent).getType().isArrayType())) {
+ return urlAssembler.getIncorrectVarDeclarationURL(problemNode);
+ }
+
+ /* Incorrect control structures almost always have one of these statements as the
+ problem node, its parent, or its grandparent. Use reflection here instead of regular
+ instanceof to make the code more concise and readable. */
+ Class>[] statementClasses = {ForStatement.class, TryStatement.class, DoStatement.class,
+ SwitchStatement.class, IfStatement.class, EnhancedForStatement.class, WhileStatement.class};
+ ASTNode[] nearbyNodes = {problemNode, parent, grandparent};
+ for (ASTNode node : nearbyNodes) {
+ for (Class> statementClass : statementClasses) {
+ if (statementClass.isInstance(node)) {
+
+ /* Issues with control structures are most likely integer-related,
+ and the type isn't usually given in the problem arguments. */
+ return urlAssembler.getUnexpectedTokenURL("int");
+
+ }
+ }
+ }
+
+ break;
+ case IProblem.ParsingErrorDeleteToken:
+ return urlAssembler.getUnexpectedTokenURL(problemArguments[0]);
+ case IProblem.NoMessageSendOnBaseType:
+ return urlAssembler.getMethodCallWrongTypeURL(problemArguments[0], problemArguments[1], problemNode);
+ }
+
+ return Optional.empty();
+ }
+
+ private static String truncateClass(String qualifiedName) {
+ int lastPeriodIndex = qualifiedName.lastIndexOf('.');
+
+ if (lastPeriodIndex == -1) {
+ return qualifiedName;
+ }
+
+ return qualifiedName.substring(lastPeriodIndex + 1);
+ }
+
static private boolean isUndefinedTypeProblem(IProblem iproblem) {
int id = iproblem.getID();
diff --git a/java/src/processing/mode/java/pdex/JavaHint.java b/java/src/processing/mode/java/pdex/JavaHint.java
new file mode 100644
index 0000000000..fa4ee215cd
--- /dev/null
+++ b/java/src/processing/mode/java/pdex/JavaHint.java
@@ -0,0 +1,689 @@
+package processing.mode.java.pdex;
+
+import org.eclipse.jdt.core.compiler.IProblem;
+import org.eclipse.jdt.core.dom.ASTNode;
+import org.eclipse.jdt.core.dom.ArrayAccess;
+import org.eclipse.jdt.core.dom.ArrayCreation;
+import org.eclipse.jdt.core.dom.Expression;
+import org.eclipse.jdt.core.dom.ITypeBinding;
+import org.eclipse.jdt.core.dom.MethodDeclaration;
+import org.eclipse.jdt.core.dom.MethodInvocation;
+import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
+import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
+import processing.app.ui.EditorHints;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+public class JavaHint implements EditorHints.Hint {
+ private static final List PRIMITIVES = Arrays.asList(
+ "byte", "short", "int", "long",
+ "float", "double", "boolean", "char"
+ );
+ private static final Random RANDOM = new Random();
+ private static final String[] ALPHABET = {
+ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+ "n", "o", "p", "q", "r", "s", "t", "u", "v", "x", "y", "z"
+ };
+
+ public static List fromIProblem(IProblem compilerError, ASTNode ast) {
+ String[] problemArguments = compilerError.getArguments();
+ ASTNode problemNode = ASTUtils.getASTNodeAt(
+ ast,
+ compilerError.getSourceStart(),
+ compilerError.getSourceEnd()
+ );
+
+ switch (compilerError.getID()) {
+ case IProblem.MustDefineEitherDimensionExpressionsOrInitializer:
+ return getArrDimHints(problemNode);
+ case IProblem.IllegalDimension:
+ return getTwoDimArrHints(problemNode);
+ case IProblem.CannotDefineDimensionExpressionsWithInit:
+ return getTwoInitializerArrHints(problemNode);
+ case IProblem.UndefinedMethod:
+ return getMissingMethodHints(problemNode);
+ case IProblem.ParameterMismatch:
+ return getParamMismatchHints(problemNode);
+ case IProblem.ShouldReturnValue:
+ return getMissingReturnHints(problemNode);
+ case IProblem.TypeMismatch:
+ String providedType = truncateClass(problemArguments[0]);
+ String requiredType = truncateClass(problemArguments[1]);
+ return getTypeMismatchHints(providedType, requiredType, problemNode);
+ case IProblem.UndefinedType:
+ return getMissingTypeHints(problemArguments[0], problemNode);
+ case IProblem.UnresolvedVariable:
+ return getMissingVarHints(problemArguments[0], problemNode);
+ }
+
+ return Collections.emptyList();
+ }
+
+ private static List getArrDimHints(ASTNode problemNode) {
+ List hints = new ArrayList<>();
+
+ String arrType = problemNode.toString();
+ String arrName = ((VariableDeclarationFragment) problemNode.getParent().getParent().getParent())
+ .getName().toString();
+ String problemTitle = "You have not given the array a certain size.";
+
+ // Suggest adding array dimension
+ JavaHint addDim = new JavaHint(problemTitle,
+ "You may have forgotten to type the size "
+ + "of the array inside the brackets."
+ );
+ addDim.addBadCode(arrType + "[] " + arrName + " = new " + arrType + "[];");
+ addDim.addGoodCode(arrType + "[] " + arrName + " = new " + arrType + "[5];");
+ hints.add(addDim);
+
+ return hints;
+ }
+
+ private static List getTwoDimArrHints(ASTNode problemNode) {
+ List hints = new ArrayList<>();
+
+ String arrType = ((ArrayCreation) problemNode.getParent()).getType().getElementType().toString();
+ String arrName = ((VariableDeclarationFragment) problemNode.getParent().getParent())
+ .getName().toString();
+ String problemTitle = "In a 2D array, you have not given the "
+ + "innermost array a certain size.";
+
+ // Suggest adding array dimension
+ JavaHint addDim = new JavaHint(problemTitle,
+ "Specify the size of the innermost array."
+ );
+ addDim.addBadCode(arrType + "[][] " + arrName + " = new " + arrType + "[][5];");
+ addDim.addGoodCode(arrType + "[][] " + arrName + " = new " + arrType + "[5][5];");
+ addDim.addGoodCode(arrType + "[][] " + arrName + " = new " + arrType + "[5][];");
+ hints.add(addDim);
+
+ return hints;
+ }
+
+ private static List getTwoInitializerArrHints(ASTNode problemNode) {
+ List hints = new ArrayList<>();
+
+ String arrType = ((ArrayCreation) problemNode.getParent()).getType().getElementType().toString();
+ String arrName = ((VariableDeclarationFragment) problemNode.getParent().getParent())
+ .getName().toString();
+ String problemTitle = "You defined an array twice.";
+
+ // Suggest adding array dimension
+ JavaHint chooseInitMethod = new JavaHint(problemTitle,
+ "You may have used both methods to construct an array together."
+ );
+
+ String initList = buildInitializerList(arrType, 5);
+ chooseInitMethod.addBadCode(arrType + "[] " + arrName + " = new " + arrType
+ + "[5] " + initList + ";");
+ chooseInitMethod.addGoodCode(arrType + "[] " + arrName + " = new " + arrType + "[5];");
+ chooseInitMethod.addGoodCode(arrType + "[] " + arrName + " = " + initList + ";");
+ hints.add(chooseInitMethod);
+
+ return hints;
+ }
+
+ private static List getMissingMethodHints(ASTNode problemNode) {
+ List hints = new ArrayList<>();
+
+ MethodInvocation invoc = (MethodInvocation) problemNode.getParent();
+ List providedParams = ((List>) invoc.arguments()).stream()
+ .map(Object::toString).collect(Collectors.toList());
+ List providedParamTypes = ((List>) invoc.arguments()).stream().map(
+ (param) -> ((Expression) param).resolveTypeBinding().getName()
+ ).collect(Collectors.toList());
+
+ /* We don't know the desired return type, so use a
+ familiar one like "int" instead of one like "void." */
+ String dummyReturnType = "int";
+
+ String methodName = invoc.getName().toString();
+ String nameWithParens = methodName + "()";
+ String currMethodCall = methodName + "(" + String.join(", ", providedParams) + ")";
+ String renamedMethodCall = "correctName(" + String.join(", ", providedParams) + ")";
+
+ String problemTitle = "You are trying to use a function, "
+ + nameWithParens
+ + ", which Processing does not recognize. (\"Method\" "
+ + "and \"function\" are used interchangeably here.)";
+
+ // Suggest using correct Java name
+ JavaHint useJavaName = new JavaHint(problemTitle,
+ "If you are trying to use an existing Java function, "
+ + "make sure you match the name of " + nameWithParens
+ + " with the function."
+
+ );
+ useJavaName.addBadCode("String str = " + getDemoValue("String") + ";\n"
+ + "str." + currMethodCall + ";");
+ useJavaName.addGoodCode("String str = " + getDemoValue("String") + ";\n"
+ + "str." + renamedMethodCall + ";");
+ hints.add(useJavaName);
+
+ // Suggest using correct user-given name
+ JavaHint useDeclarationName = new JavaHint(problemTitle,
+ "You may need to change the name of "
+ + nameWithParens + " to the method you created."
+ );
+ useDeclarationName.addBadCode(currMethodCall + ";");
+ useDeclarationName.addGoodCode(currMethodCall + ";\n"
+ + getMethodDec(methodName, dummyReturnType, providedParamTypes) + " {\n"
+ + " ...\n"
+ + "}");
+ hints.add(useDeclarationName);
+
+ // Suggest calling method on object
+ JavaHint callOnObj = new JavaHint(problemTitle,
+ "You may need to create an object of a class "
+ + "and call the method " + nameWithParens + " on it."
+ );
+ callOnObj.addBadCode("class YourClass {\n "
+ + getMethodDec(methodName, dummyReturnType, providedParamTypes) + " {\n"
+ + " ...\n"
+ + " }\n}\n"
+ + currMethodCall + ";");
+ callOnObj.addGoodCode("class YourClass {\n "
+ + getMethodDec(methodName, dummyReturnType, providedParamTypes) + " {\n"
+ + " ...\n"
+ + " }\n}\n"
+ + getDemoDeclaration("YourClass", "myObject")
+ + "\nmyObject." + currMethodCall + ";");
+ hints.add(callOnObj);
+
+ // Suggest creating class method
+ JavaHint createClassMethod = new JavaHint(problemTitle,
+ "You may need to create the method "
+ + nameWithParens + " in a class."
+ );
+ createClassMethod.addBadCode("class YourClass {\n}\n"
+ + getDemoDeclaration("YourClass", "myObject")
+ + "\nmyObject." + currMethodCall + ";");
+ createClassMethod.addGoodCode("class YourClass {\n "
+ + getMethodDec(methodName, dummyReturnType, providedParamTypes) + " {\n"
+ + " ...\n"
+ + " }\n}\n"
+ + getDemoDeclaration("YourClass", "myObject")
+ + "\nmyObject." + currMethodCall + ";");
+ hints.add(createClassMethod);
+
+ return hints;
+ }
+
+ private static List getParamMismatchHints(ASTNode problemNode) {
+ List hints = new ArrayList<>();
+
+ MethodInvocation invoc = (MethodInvocation) problemNode.getParent();
+ List providedParams = ((List>) invoc.arguments()).stream()
+ .map(Object::toString).collect(Collectors.toList());
+ List providedParamTypes = ((List>) invoc.arguments()).stream().map(
+ (param) -> ((Expression) param).resolveTypeBinding().getName()
+ ).collect(Collectors.toList());
+ List requiredParamTypes = Arrays.stream(
+ invoc.resolveMethodBinding().getParameterTypes()
+ ).map(ITypeBinding::getName).collect(Collectors.toList());
+
+ String methodName = invoc.getName().toString();
+ String methodReturnType = invoc.resolveMethodBinding().getReturnType().toString();
+ String methodSig = getMethodSig(methodName, requiredParamTypes);
+ String methodDec = getMethodDec(methodName, methodReturnType, requiredParamTypes);
+ String problemTitle = "You are trying to use the method " + methodSig
+ + " but with incorrect parameters.";
+
+ String badCode = methodDec + " {\n ...\n}\n"
+ + "void setup() {\n "
+ + methodName + "(" + String.join(", ", providedParams) + ");\n"
+ + "}\n";
+
+ // Suggest changing provided parameter
+ JavaHint changeParam = new JavaHint(problemTitle,
+ "You might need to change a parameter of " + methodSig
+ + " to the expected type."
+ );
+ changeParam.addBadCode(badCode);
+ changeParam.addGoodCode(methodDec + " {\n ...\n}\n"
+ + "void setup() {\n "
+ + getMethodCall(methodName, requiredParamTypes) + ";\n"
+ + "}\n");
+ hints.add(changeParam);
+
+ // Suggest changing definition parameter
+ JavaHint changeDef = new JavaHint(problemTitle,
+ "You might need to change a parameter of " + methodSig
+ + " in the method declaration to the expected type."
+ );
+ changeDef.addBadCode(badCode);
+ changeDef.addGoodCode(getMethodDec(methodName, methodReturnType, providedParamTypes)
+ + " {\n ...\n}\n"
+ + "void setup() {\n "
+ + methodName + "(" + String.join(", ", providedParams) + ");\n"
+ + "}\n");
+ hints.add(changeDef);
+
+ if (providedParamTypes.size() != requiredParamTypes.size()) {
+
+ // Suggest changing number of provided parameters
+ JavaHint changeNumParams = new JavaHint(problemTitle,
+ "You may need to change the number of parameters to the "
+ + "expected amount when calling " + methodSig + "."
+ );
+ changeNumParams.addBadCode(badCode);
+ changeNumParams.addGoodCode(methodDec + " {\n ...\n}\n"
+ + "void setup() {\n "
+ + getMethodCall(methodName, requiredParamTypes) + ";\n"
+ + "}\n");
+ hints.add(changeNumParams);
+
+ // Suggest changing number of definition parameters
+ JavaHint changeNumDefParams = new JavaHint(problemTitle,
+ "Change the number of parameters in the " + methodSig
+ + " method declaration."
+ );
+ changeNumDefParams.addBadCode(badCode);
+ changeNumDefParams.addGoodCode(
+ getMethodDec(methodName, methodReturnType, providedParamTypes)
+ + " {\n ...\n}\n"
+ + "void setup() {\n "
+ + methodName + "(" + String.join(", ", providedParams) + ");\n"
+ + "}\n");
+ hints.add(changeNumDefParams);
+
+ }
+
+ return hints;
+ }
+
+ private static List getMissingReturnHints(ASTNode problemNode) {
+ List hints = new ArrayList<>();
+
+ MethodDeclaration invoc = (MethodDeclaration) problemNode.getParent();
+ List requiredParamTypes = Arrays.stream(
+ invoc.resolveBinding().getParameterTypes()
+ ).map(ITypeBinding::getName).collect(Collectors.toList());
+ String methodName = invoc.getName().toString();
+ String methodReturnType = invoc.getReturnType2().toString();
+ String methodDec = getMethodDec(methodName, methodReturnType, requiredParamTypes);
+ String nameWithParens = methodName + "()";
+
+ String problemTitle = "You did not return a value of type " + methodReturnType
+ + " like the definition of method " + nameWithParens + ".";
+
+ // Suggest adding return at end
+ JavaHint returnEnd = new JavaHint(problemTitle,
+ "You may need to add a return statement of type " + methodReturnType
+ + " at the end of the method " + nameWithParens + "."
+ );
+ returnEnd.addBadCode(methodDec + " {\n"
+ + " ...\n"
+ + "}");
+ returnEnd.addGoodCode(methodDec + " {\n"
+ + " ...\n"
+ + " return " + getDemoValue(methodReturnType) + ";\n"
+ + "}");
+ hints.add(returnEnd);
+
+ // Suggest adding return in all branches
+ JavaHint returnBranch = new JavaHint(problemTitle,
+ "Make sure all branches of conditionals in " + nameWithParens
+ + " return a value of type " + methodReturnType + "."
+ );
+ returnBranch.addBadCode(methodDec + " {\n"
+ + " if (...) {\n"
+ + " ...\n"
+ + " return " + getDemoValue(methodReturnType) + ";\n"
+ + " } else {\n"
+ + " ...\n"
+ + " }\n"
+ + "}");
+ returnBranch.addGoodCode(methodDec + " {\n"
+ + " if (...) {\n"
+ + " ...\n"
+ + " return " + getDemoValue(methodReturnType) + ";\n"
+ + " } else {\n"
+ + " ...\n"
+ + " return " + getDemoValue(methodReturnType) + ";\n"
+ + " }\n"
+ + "}");
+ returnBranch.addGoodCode(methodDec + " {\n"
+ + " if (...) {\n"
+ + " ...\n"
+ + " return " + getDemoValue(methodReturnType) + ";\n"
+ + " } \n"
+ + " ...\n"
+ + " return " + getDemoValue(methodReturnType) + ";\n"
+ + "}");
+ hints.add(returnBranch);
+
+ return hints;
+ }
+
+ private static List getTypeMismatchHints(String providedType, String requiredType,
+ ASTNode problemNode) {
+ List hints = new ArrayList<>();
+
+ String varName = ((VariableDeclarationFragment) problemNode.getParent()).getName().toString();
+ String problemTitle = "You are trying to use the " + getVarDescription(requiredType)
+ + " " + varName + " as a " + getVarDescription(providedType) + ".";
+
+ // Suggest changing variable declaration
+ JavaHint changeVarDec = new JavaHint(problemTitle,
+ "You might need to change the variable declaration of "
+ + varName + " to type " + providedType + "."
+ );
+ changeVarDec.addBadCode(getDemoDeclaration(requiredType, varName, providedType));
+ changeVarDec.addGoodCode(getDemoDeclaration(providedType, varName));
+ hints.add(changeVarDec);
+
+ // Suggest changing variable value
+ JavaHint changeValue = new JavaHint(problemTitle,
+ "You might need to change the value of "
+ + varName + " to a " + requiredType + "."
+ );
+ changeValue.addBadCode(getDemoDeclaration(requiredType, varName, providedType));
+ changeValue.addGoodCode(getDemoDeclaration(requiredType, varName));
+ hints.add(changeValue);
+
+ // Suggest changing return type
+ JavaHint changeReturnType = new JavaHint(problemTitle,
+ "You might need to change the method's return type to "
+ + providedType + "."
+ );
+ changeReturnType.addBadCode(requiredType + " doSomething() {\n"
+ + " " + getDemoDeclaration(providedType, varName) + "\n"
+ + " " + "return " + varName + ";\n"
+ + "}");
+ changeReturnType.addGoodCode(providedType + " doSomething() {\n"
+ + " " + getDemoDeclaration(providedType, varName) + "\n"
+ + " " + "return " + varName + ";\n"
+ + "}");
+ hints.add(changeReturnType);
+
+ // Clarify numerical expressions where a float result is assigned to an int
+ if (providedType.equals("float") && requiredType.equals("int")) {
+ JavaHint changeOpType = new JavaHint(problemTitle,
+ "You may have used an int-type variable " + varName
+ + " in an operation involving the float type."
+ );
+ changeOpType.addBadCode(getDemoDeclaration(requiredType, varName) + "\n"
+ + varName + " = " + varName + " + 3.14;");
+ changeOpType.addGoodCode(getDemoDeclaration(providedType, varName) + "\n"
+ + varName + " = " + varName + " + 3.14;");
+ hints.add(changeOpType);
+ }
+
+ return hints;
+ }
+
+ private static List getMissingTypeHints(String missingType, ASTNode problemNode) {
+ List hints = new ArrayList<>();
+
+ ASTNode grandparent = problemNode.getParent().getParent();
+ if (!(grandparent instanceof VariableDeclarationStatement)) {
+ return Collections.emptyList();
+ }
+
+ // All variables in the statement will be the same type, so use the first as an example
+ VariableDeclarationStatement varStatement = (VariableDeclarationStatement) problemNode.getParent().getParent();
+ VariableDeclarationFragment firstVar = (VariableDeclarationFragment) varStatement.fragments().get(0);
+
+ String varName = firstVar.getName().toString();
+ String problemTitle = "You are trying to declare a variable of type "
+ + missingType + ", which Processing does not recognize.";
+
+ // Suggest fixing a typo in the type name
+ JavaHint fixTypo = new JavaHint(problemTitle,
+ "You may need to correct the name of " + missingType
+ + " if you mistyped it."
+ );
+ fixTypo.addBadCode(getDemoDeclaration(missingType, varName));
+ fixTypo.addGoodCode(getDemoDeclaration("CorrectName", varName));
+ hints.add(fixTypo);
+
+ // Suggest importing the class from library
+ JavaHint importLib = new JavaHint(problemTitle,
+ "You may need to correct the name of " + missingType
+ + " if you mistyped it."
+ );
+ importLib.addBadCode(getDemoDeclaration(missingType, varName));
+ importLib.addGoodCode("import path.to.library." + missingType + ";\n"
+ + getDemoDeclaration(missingType, varName));
+ hints.add(importLib);
+
+ // Suggest importing the class from another file
+ JavaHint importFile = new JavaHint(problemTitle,
+ "You may need to correct the name of " + missingType
+ + " if you mistyped it."
+ );
+ importFile.addBadCode(getDemoDeclaration(missingType, varName));
+ importFile.addGoodCode("import OtherFile." + missingType + ";\n"
+ + getDemoDeclaration(missingType, varName));
+ hints.add(importFile);
+
+ // Suggest creating the class
+ JavaHint createClass = new JavaHint(problemTitle,
+ "You may need to correct the name of " + missingType
+ + " if you mistyped it."
+ );
+ createClass.addBadCode(getDemoDeclaration(missingType, varName));
+ createClass.addGoodCode("class " + missingType + " {\n ...\n}\n"
+ + getDemoDeclaration(missingType, varName));
+ hints.add(createClass);
+
+ return hints;
+ }
+
+ private static List getMissingVarHints(String varName, ASTNode problemNode) {
+ List hints = new ArrayList<>();
+
+ String problemTitle = "You are trying to use a variable named "
+ + varName + " that does not exist yet.";
+
+ ASTNode parent = problemNode.getParent();
+
+ if (parent instanceof MethodInvocation) {
+ MethodInvocation invoc = (MethodInvocation) parent;
+ List requiredParamTypes = Arrays.stream(
+ invoc.resolveMethodBinding().getParameterTypes()
+ ).map(ITypeBinding::getName).collect(Collectors.toList());
+ List providedParams = ((List>) invoc.arguments()).stream()
+ .map(Object::toString).collect(Collectors.toList());
+
+ String varType = requiredParamTypes.get(providedParams.indexOf(varName));
+ String varDec = getDemoDeclaration(varType, varName);
+ String varUse = invoc + ";";
+
+ // Suggest adding declaration
+ JavaHint addDec = new JavaHint(problemTitle,
+ "You may need to add variable declaration for " + varName
+ + " before its first occurrence in the code."
+ );
+ addDec.addBadCode(varUse);
+ addDec.addGoodCode(varDec
+ + "\n" + varUse);
+ hints.add(addDec);
+
+ // Suggest fixing typo
+ JavaHint fixName = new JavaHint(problemTitle,
+ "You may need to change " + varName
+ + " to a variable name that you have defined."
+ );
+ fixName.addBadCode(getDemoDeclaration(varType, "correctName")
+ + "\n" + varUse);
+ fixName.addGoodCode(getDemoDeclaration(varType, "correctName")
+ + "\n" + varUse.replaceAll("\\b" + varName + "\\b", "correctName"));
+ hints.add(fixName);
+
+ // Suggest moving to same function
+ JavaHint moveToSameFxn = new JavaHint(problemTitle,
+ "You may need to move " + varName
+ + " to the same function as its declaration."
+ );
+ moveToSameFxn.addBadCode("void setup() {\n " + varDec + "\n}\n"
+ + "void draw {\n " + varUse + "\n}");
+ moveToSameFxn.addGoodCode("void draw() {\n " + varDec + "\n"
+ + " " + varUse + "\n}");
+ hints.add(moveToSameFxn);
+
+ // Suggest moving to same or smaller scope
+ JavaHint moveToScope = new JavaHint(problemTitle,
+ "You may need to move " + varName
+ + " to the same or smaller scope as its declaration."
+ );
+ moveToScope.addBadCode("while (...) {\n " + varDec + "\n}\n" + varUse);
+ moveToScope.addBadCode("void setup() {\n " + varDec + "\n}\n"
+ + "void draw {\n " + varUse + "\n}");
+ moveToScope.addGoodCode("while (...) {\n " + varDec + "\n " + varUse + "\n}");
+ moveToScope.addGoodCode(varDec + "\nvoid draw() {\n ...\n"
+ + " " + varUse + "\n}");
+ hints.add(moveToScope);
+
+ } else if (parent instanceof ArrayAccess && parent.getParent() instanceof VariableDeclarationFragment) {
+
+ ArrayAccess arrAccess = (ArrayAccess) parent;
+ String length = arrAccess.getIndex().toString();
+
+ // The "varName" is actually the declared type when an array is being created
+ VariableDeclarationFragment declaration = (VariableDeclarationFragment)
+ parent.getParent();
+ String arrName = declaration.getName().toString();
+
+ // Suggest adding "new" to an array declaration
+ JavaHint addNewToArrDec = new JavaHint(problemTitle,
+ "You may have missed the word \"new\" when creating an array."
+ );
+ addNewToArrDec.addBadCode(varName + "[] " + arrName
+ + " = " + varName + "[" + length + "];");
+ addNewToArrDec.addGoodCode(varName + "[] " + arrName
+ + " = new " + varName + "[" + length + "];");
+ hints.add(addNewToArrDec);
+
+ }
+
+ return hints;
+ }
+
+ private static String getMethodSig(String methodName, List paramTypes) {
+ return methodName + "(" + String.join(", ", paramTypes) + ")";
+ }
+
+ private static String getMethodDec(String name, String returnType, List paramTypes) {
+ IntStream indices = IntStream.range(0, paramTypes.size());
+ List typesWithNames = indices.mapToObj(
+ (index) -> paramTypes.get(index) + " param" + (index + 1)
+ ).collect(Collectors.toList());
+
+ return returnType + " " + name + "(" + String.join(", ", typesWithNames) + ")";
+ }
+
+ private static String getMethodCall(String name, List paramTypes) {
+ List paramValues = paramTypes.stream().map(
+ JavaHint::getDemoValue
+ ).collect(Collectors.toList());
+
+ return name + "(" + String.join(", ", paramValues) + ")";
+ }
+
+ private static String buildInitializerList(String type, int size) {
+ StringBuilder initializerList = new StringBuilder("{");
+ String separator = ", ";
+
+ for (int item = 0; item < size; item++) {
+ initializerList.append(getDemoValue(type)).append(separator);
+ }
+
+ int lastSeparatorIndex = initializerList.lastIndexOf(separator);
+ return initializerList.substring(0, lastSeparatorIndex) + "}";
+ }
+
+ private static String getVarDescription(String typeName) {
+ if (PRIMITIVES.contains(typeName) || typeName.equals("String")) {
+ return typeName + "-type variable";
+ }
+
+ return typeName + " object";
+ }
+
+ private static String getDemoDeclaration(String decType, String varName) {
+ return getDemoDeclaration(decType, varName, decType);
+ }
+
+ private static String getDemoDeclaration(String decType, String varName, String valType) {
+ return decType + " " + varName + " = " + getDemoValue(valType) + ";";
+ }
+
+ private static String getDemoValue(String typeName) {
+ switch (typeName) {
+ case "byte":
+ case "short":
+ case "int":
+ case "long":
+ return Integer.toString(RANDOM.nextInt(100));
+ case "float":
+ case "double":
+ return String.format("%1$,.2f", Math.random() * 10);
+ case "boolean":
+ return Boolean.toString(Math.random() > 0.5);
+ case "char":
+ return ALPHABET[RANDOM.nextInt(ALPHABET.length)];
+ case "String":
+
+ // Hard-code this to avoid inappropriate random strings
+ return "\"hello world\"";
+
+ default:
+ return "new " + typeName + "()";
+ }
+ }
+
+ private static String truncateClass(String qualifiedName) {
+ int lastPeriodIndex = qualifiedName.lastIndexOf('.');
+
+ if (lastPeriodIndex == -1) {
+ return qualifiedName;
+ }
+
+ return qualifiedName.substring(lastPeriodIndex + 1);
+ }
+
+ private final String PROBLEM_TEXT;
+ private final String SUGGESTION_TEXT;
+ private final List GOOD_CODE;
+ private final List BAD_CODE;
+
+ public JavaHint(String problemText, String suggestionText) {
+ PROBLEM_TEXT = problemText;
+ SUGGESTION_TEXT = suggestionText;
+ GOOD_CODE = new ArrayList<>();
+ BAD_CODE = new ArrayList<>();
+ }
+
+ public void addGoodCode(String goodCode) {
+ GOOD_CODE.add(goodCode);
+ }
+
+ public void addBadCode(String badCode) {
+ BAD_CODE.add(badCode);
+ }
+
+ public String getProblemText() {
+ return PROBLEM_TEXT;
+ }
+
+ public String getSuggestionText() {
+ return SUGGESTION_TEXT;
+ }
+
+ public List getBadCode() {
+ return BAD_CODE;
+ }
+
+ public List getGoodCode() {
+ return GOOD_CODE;
+ }
+
+}
diff --git a/java/src/processing/mode/java/pdex/JavaProblem.java b/java/src/processing/mode/java/pdex/JavaProblem.java
index b8ef76c0ae..9fc88a76e8 100644
--- a/java/src/processing/mode/java/pdex/JavaProblem.java
+++ b/java/src/processing/mode/java/pdex/JavaProblem.java
@@ -23,6 +23,11 @@
import org.eclipse.jdt.core.compiler.IProblem;
import processing.app.Problem;
+import processing.app.ui.EditorHints;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
/**
@@ -58,6 +63,11 @@ public class JavaProblem implements Problem {
*/
private String[] importSuggestions;
+ /**
+ * Common errors have code examples in a separate tab.
+ */
+ private String matchingRefUrl;
+
public static final int ERROR = 1, WARNING = 2;
public JavaProblem(String message, int type, int tabIndex, int lineNumber) {
@@ -65,6 +75,7 @@ public JavaProblem(String message, int type, int tabIndex, int lineNumber) {
this.type = type;
this.tabIndex = tabIndex;
this.lineNumber = lineNumber;
+ this.matchingRefUrl = "";
}
/**
@@ -134,6 +145,16 @@ public void setImportSuggestions(String[] a) {
importSuggestions = a;
}
+ @Override
+ public String getMatchingRefURL() {
+ return matchingRefUrl;
+ }
+
+ @Override
+ public void setMatchingRefURL(String url) {
+ matchingRefUrl = url;
+ }
+
@Override
public String toString() {
return "TAB " + tabIndex + ",LN " + lineNumber + "LN START OFF: "
diff --git a/java/src/processing/mode/java/pdex/MatchingRefURLAssembler.java b/java/src/processing/mode/java/pdex/MatchingRefURLAssembler.java
new file mode 100644
index 0000000000..c938a4b919
--- /dev/null
+++ b/java/src/processing/mode/java/pdex/MatchingRefURLAssembler.java
@@ -0,0 +1,864 @@
+package processing.mode.java.pdex;
+
+import org.eclipse.jdt.core.dom.ASTNode;
+import org.eclipse.jdt.core.dom.ArrayAccess;
+import org.eclipse.jdt.core.dom.ArrayCreation;
+import org.eclipse.jdt.core.dom.ArrayInitializer;
+import org.eclipse.jdt.core.dom.Assignment;
+import org.eclipse.jdt.core.dom.CastExpression;
+import org.eclipse.jdt.core.dom.ConditionalExpression;
+import org.eclipse.jdt.core.dom.Expression;
+import org.eclipse.jdt.core.dom.ExpressionStatement;
+import org.eclipse.jdt.core.dom.FieldDeclaration;
+import org.eclipse.jdt.core.dom.ITypeBinding;
+import org.eclipse.jdt.core.dom.InfixExpression;
+import org.eclipse.jdt.core.dom.InstanceofExpression;
+import org.eclipse.jdt.core.dom.MethodDeclaration;
+import org.eclipse.jdt.core.dom.MethodInvocation;
+import org.eclipse.jdt.core.dom.PostfixExpression;
+import org.eclipse.jdt.core.dom.PrefixExpression;
+import org.eclipse.jdt.core.dom.QualifiedName;
+import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
+import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
+import processing.app.SketchException;
+import processing.app.syntax.JEditTextArea;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * Creates URLs for MatchingRef errors based on the AST.
+ * @author soir20
+ */
+public class MatchingRefURLAssembler {
+ private static final String URL = "http://139.147.9.247/";
+ private final String GLOBAL_PARAMS;
+
+ /**
+ * Creates a new URL assembler for MatchingRef.
+ * @param embedded whether the pages will be embedded
+ */
+ public MatchingRefURLAssembler(boolean embedded) {
+ if (embedded) {
+ GLOBAL_PARAMS = "&embed=true";
+ } else {
+ GLOBAL_PARAMS = "";
+ }
+ }
+
+ /**
+ * Gets the MatchingRef URL for an extra right curly brace.
+ * @param textAboveError all text in the editor at and above the
+ * line with the extra brace
+ * @return the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getClosingCurlyBraceURL(String textAboveError) {
+
+ // We want to find a block before the extraneous brace
+ int endIndex = textAboveError.lastIndexOf('}');
+ int rightBraceIndex = textAboveError.lastIndexOf('}', endIndex - 1);
+ int leftBraceIndex = findMatchingBrace(textAboveError, rightBraceIndex);
+
+ int startIndex = textAboveError.lastIndexOf('\n', leftBraceIndex);
+ if (startIndex > 0) {
+ startIndex = textAboveError.lastIndexOf('\n', startIndex - 1);
+ }
+ String mismatchedSnippet = textAboveError.substring(startIndex + 1, leftBraceIndex + 1)
+ + "\n /* your code */\n" + textAboveError.substring(rightBraceIndex, endIndex + 1);
+ String correctedSnippet = mismatchedSnippet.substring(0, mismatchedSnippet.length() - 1);
+
+ try {
+ mismatchedSnippet = URLEncoder.encode(mismatchedSnippet, "UTF-8");
+ correctedSnippet = URLEncoder.encode(correctedSnippet, "UTF-8");
+ } catch (UnsupportedEncodingException err) {
+ return Optional.empty();
+ }
+
+ return Optional.of(URL + "extraneousclosingcurlybrace?original=" + mismatchedSnippet
+ + "&fixed=" + correctedSnippet + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for an incorrect variable declaration.
+ * @param textArea text area for the file that contains the error
+ * @param exception incorrect declaration exception from compilation
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getIncorrectVarDeclarationURL(JEditTextArea textArea, SketchException exception) {
+ int errorIndex = textArea.getLineStartOffset(exception.getCodeLine()) + exception.getCodeColumn() - 1;
+ String code = textArea.getText();
+ String declarationStatement = code.substring(errorIndex);
+ int statementEndIndex = declarationStatement.indexOf(';', errorIndex);
+ if (statementEndIndex >= 0) {
+ declarationStatement = declarationStatement.substring(0, statementEndIndex);
+ }
+
+ List declaredArrays = getDeclaredArrays(declarationStatement);
+
+ String pattern = "\\s*[\\w\\d$]+\\s*=\\s*(new\\s*[\\w\\d$]+\\s*\\[\\d+]|\\{.*})\\s*[,;]";
+ Optional firstInvalidDeclarationOptional =
+ declaredArrays.stream().filter((declaration) -> !declaration.matches(pattern)).findFirst();
+
+ if (!firstInvalidDeclarationOptional.isPresent()) {
+ return Optional.empty();
+ }
+
+ String firstInvalidDeclaration = firstInvalidDeclarationOptional.get();
+ String arrName = firstInvalidDeclaration.trim().split("[^\\w\\d$]")[0];
+
+ // Get array type
+ String beforeErrorText = code.substring(0, errorIndex);
+ int currentIndex = beforeErrorText.length() - 1;
+
+ boolean hasIdentifierEnded = false;
+ StringBuilder arrType = new StringBuilder();
+ while (currentIndex >= 0 && !hasIdentifierEnded) {
+ String currentChar = beforeErrorText.substring(currentIndex, currentIndex + 1);
+
+ if (!currentChar.matches("[\\s\\[\\]]")) {
+ arrType.insert(0, currentChar);
+ } else if (arrType.length() > 0) {
+ hasIdentifierEnded = true;
+ }
+
+ currentIndex--;
+ }
+
+ return Optional.of(URL + "incorrectvariabledeclaration?typename=" + trimType(arrType.toString())
+ + "&foundname=" + arrName + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for an incorrect variable declaration.
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getIncorrectVarDeclarationURL(ASTNode problemNode) {
+ Optional fragmentOptional = findDeclarationFragment(problemNode);
+ if (!fragmentOptional.isPresent()) {
+ return Optional.empty();
+ }
+
+ VariableDeclarationFragment fragment = fragmentOptional.get();
+
+ String arrName = fragment.getName().toString();
+ String arrType = trimType(fragment.resolveBinding().getType().getElementType().toString());
+
+ return Optional.of(URL + "incorrectvariabledeclaration?typename=" + arrType
+ + "&foundname=" + arrName + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for an incorrect method declaration.
+ * @param textAboveError all text in the editor at and above the
+ * line with error
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getIncorrectMethodDeclarationURL(String textAboveError) {
+ int lastOpenParenthesisIndex = textAboveError.lastIndexOf('(');
+
+ int currentCharIndex = lastOpenParenthesisIndex;
+ char currentChar;
+ do {
+ currentCharIndex--;
+ currentChar = textAboveError.charAt(currentCharIndex);
+ } while (currentCharIndex > 0 && Character.isJavaIdentifierPart(currentChar));
+
+ /* The method name starts one character ahead of the current one if
+ we didn't reach the start of the string */
+ if (currentCharIndex > 0) currentCharIndex++;
+
+ String methodName = textAboveError.substring(currentCharIndex, lastOpenParenthesisIndex);
+
+ return Optional.of(URL + "incorrectmethoddeclaration?methodname=" + methodName + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for a missing array dimension.
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getArrDimURL(ASTNode problemNode) {
+ Optional fragmentOptional = findDeclarationFragment(problemNode);
+ if (!fragmentOptional.isPresent()) {
+ return Optional.empty();
+ }
+
+ String arrType = trimType(problemNode.toString());
+ String arrName = fragmentOptional.get().getName().toString();
+
+ return Optional.of(URL + "incorrectdimensionexpression1?typename=" + arrType
+ + "&arrname=" + arrName
+ + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL when the first of two array dimensions is missing.
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getTwoDimArrURL(ASTNode problemNode) {
+ ASTNode parent = problemNode.getParent();
+ Optional fragmentOptional = findDeclarationFragment(problemNode);
+ if (!(parent instanceof ArrayCreation) || !fragmentOptional.isPresent()) {
+ return Optional.empty();
+ }
+
+ String arrType = trimType(((ArrayCreation) parent).getType().getElementType().toString());
+ String arrName = fragmentOptional.get().getName().toString();
+
+ return Optional.of(URL + "incorrectdimensionexpression2?typename=" + arrType
+ + "&arrname=" + arrName
+ + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for the use of two array initializers at once.
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getTwoInitializerArrURL(ASTNode problemNode) {
+ ASTNode parent = problemNode.getParent();
+ Optional fragmentOptional = findDeclarationFragment(problemNode);
+ if (!(parent instanceof ArrayCreation) || !fragmentOptional.isPresent()) {
+ return Optional.empty();
+ }
+
+ String arrType = trimType(((ArrayCreation) parent).getType().getElementType().toString());
+ String arrName = fragmentOptional.get().getName().toString();
+
+ return Optional.of(URL + "incorrectdimensionexpression3?typename=" + arrType
+ + "&arrname=" + arrName
+ + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for a missing method.
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getMissingMethodURL(ASTNode problemNode) {
+ ASTNode parent = problemNode.getParent();
+ if (!(parent instanceof MethodInvocation)) {
+ return Optional.empty();
+ }
+
+ MethodInvocation invocation = (MethodInvocation) problemNode.getParent();
+ List providedParams = ((List>) invocation.arguments()).stream()
+ .map(Object::toString).collect(Collectors.toList());
+ List providedParamTypes = ((List>) invocation.arguments()).stream().map(
+ (param) -> trimType(((Expression) param).resolveTypeBinding().getName())
+ ).collect(Collectors.toList());
+ String methodName = invocation.getName().toString();
+
+ String returnType = getClosestExpressionType(invocation);
+ String dummyCorrectName = "correctName";
+
+ String encodedParams;
+ String encodedTypes;
+ try {
+ encodedParams = URLEncoder.encode(String.join(",", providedParams), "UTF-8");
+ encodedTypes = URLEncoder.encode(String.join(",", providedParamTypes), "UTF-8");
+ } catch (UnsupportedEncodingException err) {
+ return Optional.empty();
+ }
+
+ return Optional.of(URL + "methodnotfound?methodname=" + methodName
+ + "&correctmethodname=" + dummyCorrectName
+ + "&typename=" + trimType(returnType)
+ + "&providedparams=" + encodedParams
+ + "&providedtypes=" + encodedTypes
+ + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for a parameter mismatch in a method call.
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getParamMismatchURL(ASTNode problemNode) {
+ ASTNode parent = problemNode.getParent();
+ if (!(parent instanceof MethodInvocation)) {
+ return Optional.empty();
+ }
+
+ MethodInvocation invocation = (MethodInvocation) parent;
+ List providedParamTypes = ((List>) invocation.arguments()).stream().map(
+ (param) -> trimType(((Expression) param).resolveTypeBinding().getName())
+ ).collect(Collectors.toList());
+ List requiredParamTypes = Arrays.stream(
+ invocation.resolveMethodBinding().getParameterTypes()
+ ).map((binding) -> trimType(binding.getName())).collect(Collectors.toList());
+
+ String methodName = invocation.getName().toString();
+ String methodReturnType = invocation.resolveMethodBinding().getReturnType().toString();
+
+ String encodedProvidedTypes;
+ String encodedRequiredTypes;
+ try {
+ encodedProvidedTypes = URLEncoder.encode(String.join(",", providedParamTypes), "UTF-8");
+ encodedRequiredTypes = URLEncoder.encode(String.join(",", requiredParamTypes), "UTF-8");
+ } catch (UnsupportedEncodingException err) {
+ return Optional.empty();
+ }
+
+ return Optional.of(URL + "parametermismatch?methodname=" + methodName
+ + "&methodtypename=" + methodReturnType
+ + "&providedtypes=" + encodedProvidedTypes
+ + "&requiredtypes=" + encodedRequiredTypes
+ + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for a missing return statement in a method.
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getMissingReturnURL(ASTNode problemNode) {
+ ASTNode parent = problemNode.getParent();
+ if (!(parent instanceof MethodDeclaration)) {
+ return Optional.empty();
+ }
+
+ MethodDeclaration declaration = (MethodDeclaration) parent;
+ List requiredParamTypes = Arrays.stream(
+ declaration.resolveBinding().getParameterTypes()
+ ).map((binding) -> trimType(binding.getName())).collect(Collectors.toList());
+ String methodName = declaration.getName().toString();
+ String methodReturnType = trimType(declaration.getReturnType2().toString());
+
+ String encodedTypes;
+ try {
+ encodedTypes = URLEncoder.encode(String.join(",", requiredParamTypes), "UTF-8");
+ } catch (UnsupportedEncodingException err) {
+ return Optional.empty();
+ }
+
+ return Optional.of(URL + "returnmissing?methodname=" + methodName
+ + "&typename=" + methodReturnType
+ + "&requiredtypes=" + encodedTypes
+ + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for a mismatch between a variable's type and its assigned value.
+ * @param providedType the type provided by the programmer
+ * @param requiredType the type required by the method
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getTypeMismatchURL(String providedType, String requiredType, ASTNode problemNode) {
+ String varName = problemNode.toString();
+ return Optional.of(URL + "typemismatch?typeonename=" + trimType(providedType)
+ + "&typetwoname=" + trimType(requiredType)
+ + "&varname=" + varName
+ + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for a missing type.
+ * @param missingType name of the missing type
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getMissingTypeURL(String missingType, ASTNode problemNode) {
+
+ // All variables in the statement will be the same type, so use the first as an example
+ Optional fragmentOptional = findDeclarationFragment(problemNode);
+ if (!fragmentOptional.isPresent()) {
+ return Optional.empty();
+ }
+
+ String varName = fragmentOptional.get().getName().toString();
+ String dummyCorrectName = "CorrectName";
+
+ return Optional.of(URL + "typenotfound?classname=" + trimType(missingType)
+ + "&correctclassname=" + dummyCorrectName
+ + "&varname=" + varName
+ + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for a missing variable.
+ * @param varName name of the missing variable
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getMissingVarURL(String varName, ASTNode problemNode) {
+ String varType = trimType(getClosestExpressionType(varName, problemNode));
+ return Optional.of(URL + "variablenotfound?classname=" + varType + "&varname=" + varName + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for an uninitialized variable.
+ * @param varName name of the uninitialized variable
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getUninitializedVarURL(String varName, ASTNode problemNode) {
+ String params = "?varname=" + varName;
+ ASTNode parent = problemNode.getParent();
+
+ Expression expressionNode;
+ if (parent instanceof Expression) {
+ expressionNode = (Expression) parent;
+ } else if (parent instanceof ExpressionStatement) {
+ expressionNode = ((ExpressionStatement) parent).getExpression();
+ } else {
+ return Optional.empty();
+ }
+
+ String type = expressionNode.resolveTypeBinding().getName();
+ params += "&typename=" + trimType(type);
+
+ return Optional.of(URL + "variablenotinit" + params + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for an unexpected type name.
+ * @param typeName the unexpected type name
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getUnexpectedTokenURL(String typeName) {
+ if (!couldBeType(typeName)) {
+ return Optional.empty();
+ }
+
+ return Optional.of(URL + "unexpectedtoken?typename=" + trimType(typeName) + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for a non-static method call in a static context.
+ * @param fileName name of the file where the error is located
+ * @param nonStaticMethod name of the non-static method
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getStaticErrorURL(String fileName, String nonStaticMethod, ASTNode problemNode) {
+ String params = "?methodname=" + nonStaticMethod;
+
+ ASTNode node = problemNode;
+ while (!(node instanceof MethodDeclaration)) {
+ node = node.getParent();
+ if (node == null) return Optional.empty();
+ }
+
+ String staticMethod = ((MethodDeclaration) node).getName().toString();
+ params += "&staticmethodname=" + staticMethod;
+ params += "&filename=" + fileName;
+
+ return Optional.of(URL + "nonstaticfromstatic" + params + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for a VariableDeclarators error.
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getVariableDeclaratorsURL(ASTNode problemNode) {
+ String methodName = problemNode.toString();
+
+ ASTNode parent = problemNode.getParent();
+ if (parent instanceof QualifiedName) {
+ methodName = parent.toString();
+ }
+
+ String params = "?methodonename=" + methodName;
+
+ return Optional.of(URL + "syntaxerrorvariabledeclarators" + params + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Gets the MatchingRef URL for a VariableDeclarators error.
+ * @param type the type of variable the method was invoked on
+ * @param methodName the name of the method that was invoked
+ * @param problemNode node of the AST where the problem occurred
+ * @return the the URL with path and parameters for the corresponding MatchingRef page
+ */
+ public Optional getMethodCallWrongTypeURL(String type, String methodName, ASTNode problemNode) {
+ String variableName = problemNode.toString();
+ return Optional.of(URL + "methodcallonwrongtype?methodname=" + methodName
+ + "&typename=" + trimType(type)
+ + "&varname" + variableName
+ + GLOBAL_PARAMS);
+ }
+
+ /**
+ * Trims a qualified name to its simple name.
+ * @param type the original (possibly qualified) name of the type
+ * @return the type's simple name
+ */
+ private String trimType(String type) {
+ if (type.length() == 0) {
+ return "";
+ }
+
+ return type.substring(type.lastIndexOf('.') + 1);
+ }
+
+ /**
+ * Finds the index for a brace matching the one at the index provided.
+ * @param code code to search in
+ * @param startIndex index where the the brace to find the match for is
+ * @return the index of the matching brace or -1 if there is no matching brace
+ */
+ private int findMatchingBrace(String code, int startIndex) {
+ AtomicInteger previousIndex = new AtomicInteger(startIndex);
+ int neededLeftBraces = 0;
+
+ char startChar = code.charAt(startIndex);
+ boolean findRightBrace;
+ Runnable moveToNextIndex;
+
+ // Count the initial brace
+ if (startChar == '{' || startChar == '(') {
+ neededLeftBraces--;
+ findRightBrace = true;
+ moveToNextIndex = previousIndex::getAndIncrement;
+ } else if (startChar == '}' || startChar == ')') {
+ neededLeftBraces++;
+ findRightBrace = false;
+ moveToNextIndex = previousIndex::getAndDecrement;
+ } else {
+ throw new IllegalArgumentException("Character at index "
+ + startIndex + " is not a brace or parenthesis.");
+ }
+
+ // Find the matching brace
+ while (neededLeftBraces != 0 && previousIndex.get() > 0 && previousIndex.get() < code.length() - 1) {
+ moveToNextIndex.run();
+
+ char nextChar = code.charAt(previousIndex.get());
+ if (nextChar == '{' || nextChar == '(') {
+ neededLeftBraces--;
+ } else if (nextChar == '}' || nextChar == ')') {
+ neededLeftBraces++;
+ }
+ }
+
+ char lastCharacter = code.charAt(previousIndex.get());
+ boolean isMatchingRight = findRightBrace && (lastCharacter == '}' || lastCharacter == ')');
+ boolean isMatchingLeft = !findRightBrace && (lastCharacter == '{' || lastCharacter == '(');
+ if (!isMatchingLeft && !isMatchingRight) {
+ return -1;
+ }
+
+ return previousIndex.get();
+ }
+
+ /**
+ * Extracts array declarations from a declaration statement.
+ * @param declarationStatement the statement to extract from
+ * @return the individual declarations of all arrays in the statement
+ * of the form (identifier = new Type[size],)
+ */
+ private List getDeclaredArrays(String declarationStatement) {
+ List declaredArrays = new ArrayList<>();
+ int currentIndex = 0;
+ int lastCommaIndex = -1;
+ while (currentIndex < declarationStatement.length()) {
+ char currentChar = declarationStatement.charAt(currentIndex);
+ if (currentChar == '{') {
+
+ // Skip array initializers
+ int matchingBraceIndex = findMatchingBrace(declarationStatement, currentIndex);
+ currentIndex = matchingBraceIndex == -1 ? declarationStatement.length() - 1 : matchingBraceIndex;
+
+ } else if (currentChar == ',') {
+ declaredArrays.add(declarationStatement.substring(lastCommaIndex + 1, currentIndex + 1));
+ lastCommaIndex = currentIndex;
+ currentIndex++;
+ } else {
+ currentIndex++;
+ }
+ }
+
+ declaredArrays.add(declarationStatement.substring(lastCommaIndex + 1));
+
+ return declaredArrays;
+ }
+
+ /**
+ * Finds the closest {@link VariableDeclarationFragment} to the problem node.
+ * @param problemNode problem node where error occurred
+ * @return the closest declaration fragment to the problem node
+ */
+ private Optional findDeclarationFragment(ASTNode problemNode) {
+ ASTNode node = problemNode;
+ while (node != null) {
+ if (node instanceof VariableDeclarationFragment) {
+ return Optional.of((VariableDeclarationFragment) node);
+ }
+
+ if (node instanceof FieldDeclaration) {
+ FieldDeclaration fieldDeclaration = (FieldDeclaration) node;
+ return Optional.of((VariableDeclarationFragment) fieldDeclaration.fragments().get(0));
+ }
+
+ if (node instanceof VariableDeclarationStatement) {
+ VariableDeclarationStatement declarationStatement = (VariableDeclarationStatement) node;
+ return Optional.of((VariableDeclarationFragment) declarationStatement.fragments().get(0));
+ }
+
+ node = node.getParent();
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * Gets the expression closest to the error.
+ * @param problemNode the node where the error occurred
+ * @return the type of the variable missing; defaults to "Object"
+ */
+ private String getClosestExpressionType(ASTNode problemNode) {
+
+ // The empty string will simply be ignored by methods that use it
+ return getClosestExpressionType("", problemNode);
+
+ }
+
+ /**
+ * Gets the expression closest to the error.
+ * @param missingVar the name of the missing variable
+ * @param problemNode the node where the error occurred
+ * @return the type of the variable missing; defaults to "Object"
+ */
+ private String getClosestExpressionType(String missingVar, ASTNode problemNode) {
+ Class>[] supportedExpressions = {
+ PrefixExpression.class, InfixExpression.class, PostfixExpression.class,
+ ConditionalExpression.class, InstanceofExpression.class, VariableDeclarationFragment.class,
+ ArrayCreation.class, ArrayAccess.class, ArrayInitializer.class,
+ CastExpression.class, MethodInvocation.class, Assignment.class
+ };
+
+ Map, BiFunction> typeGetters = new HashMap<>();
+ typeGetters.put(PrefixExpression.class, this::getTypeFromPrefixExpression);
+ typeGetters.put(InfixExpression.class, this::getTypeFromInfixExpression);
+ typeGetters.put(PostfixExpression.class, this::getTypeFromPostfixExpression);
+ typeGetters.put(ConditionalExpression.class, this::getTypeFromConditionalExpression);
+ typeGetters.put(InstanceofExpression.class, this::getTypeFromInstanceOf);
+ typeGetters.put(VariableDeclarationFragment.class, this::getTypeFromVarDeclaration);
+ typeGetters.put(ArrayCreation.class, this::getTypeFromArrayCreation);
+ typeGetters.put(ArrayAccess.class, this::getTypeFromArrayAccess);
+ typeGetters.put(ArrayInitializer.class, this::getTypeFromArrayInitializer);
+ typeGetters.put(CastExpression.class, this::getTypeFromCastExpression);
+ typeGetters.put(MethodInvocation.class, this::getTypeFromMethodInvocation);
+ typeGetters.put(Assignment.class, this::getTypeFromAssignment);
+
+ ASTNode node = problemNode;
+ while (node.getParent() != null) {
+ node = node.getParent();
+
+ for (Class> expressionType : supportedExpressions) {
+ if (expressionType.isInstance(node)) {
+ return typeGetters.get(expressionType).apply(missingVar, node);
+ }
+ }
+ }
+
+ return "Object";
+ }
+
+ /**
+ * Gets the type of a missing variable from a prefix expression.
+ * @param varName name of the missing variable
+ * @param prefixExpression expression closest to error
+ * @return the type of the missing variable
+ */
+ private String getTypeFromPrefixExpression(String varName, ASTNode prefixExpression) {
+ PrefixExpression prefix = (PrefixExpression) prefixExpression;
+
+ PrefixExpression.Operator[] booleanOperators = {PrefixExpression.Operator.NOT};
+
+ if (Arrays.asList(booleanOperators).contains(prefix.getOperator())) {
+ return "boolean";
+ } else {
+
+ /* Some of the other operators can also apply to floating point values,
+ but they all apply to integers. It's safer to assume the value is an integer. */
+ return "int";
+
+ }
+ }
+
+ /**
+ * Gets the type of a missing variable from an infix expression.
+ * @param varName name of the missing variable
+ * @param infixExpression expression closest to error
+ * @return the type of the missing variable
+ */
+ private String getTypeFromInfixExpression(String varName, ASTNode infixExpression) {
+ InfixExpression infix = (InfixExpression) infixExpression;
+
+ // Guess the type based on the other operand
+ if (infix.getLeftOperand() != null && infix.getLeftOperand().resolveTypeBinding() != null) {
+ return infix.getLeftOperand().resolveTypeBinding().getName();
+ } else if (infix.getRightOperand() != null && infix.getRightOperand().resolveTypeBinding() != null) {
+ return infix.getRightOperand().resolveTypeBinding().getName();
+ }
+
+ InfixExpression.Operator[] booleanOperators = {
+ InfixExpression.Operator.CONDITIONAL_OR, InfixExpression.Operator.CONDITIONAL_AND
+ };
+ InfixExpression.Operator[] numericalOperators = {
+ InfixExpression.Operator.TIMES, InfixExpression.Operator.DIVIDE, InfixExpression.Operator.REMAINDER,
+ InfixExpression.Operator.PLUS, InfixExpression.Operator.MINUS,
+ InfixExpression.Operator.LEFT_SHIFT,
+ InfixExpression.Operator.RIGHT_SHIFT_SIGNED, InfixExpression.Operator.RIGHT_SHIFT_UNSIGNED,
+ InfixExpression.Operator.LESS, InfixExpression.Operator.GREATER,
+ InfixExpression.Operator.LESS_EQUALS, InfixExpression.Operator.GREATER_EQUALS,
+ InfixExpression.Operator.XOR, InfixExpression.Operator.OR, InfixExpression.Operator.AND
+ };
+
+ // Guess the type based on the operator
+ if (Arrays.asList(booleanOperators).contains(infix.getOperator())) {
+ return "boolean";
+ } else if (Arrays.asList(numericalOperators).contains(infix.getOperator())) {
+ return "int";
+ }
+
+ // Assume it's a boolean if we can't find out any info about the type
+ return "boolean";
+
+ }
+
+ /**
+ * Gets the type of a missing variable from a postfix expression.
+ * @param varName name of the missing variable
+ * @param postfixExpression expression closest to error
+ * @return the type of the missing variable
+ */
+ private String getTypeFromPostfixExpression(String varName, ASTNode postfixExpression) {
+
+ // The only two postfix operators are increment and decrement
+ return "int";
+
+ }
+
+ /**
+ * Gets the type of a missing variable from a conditional expression.
+ * @param varName name of the missing variable
+ * @param conditionalExpression expression closest to error
+ * @return the type of the missing variable
+ */
+ private String getTypeFromConditionalExpression(String varName, ASTNode conditionalExpression) {
+ return "boolean";
+ }
+
+ /**
+ * Gets the type of a missing variable from an instanceOf expression.
+ * @param varName name of the missing variable
+ * @param instanceOf expression closest to error
+ * @return the type of the missing variable
+ */
+ private String getTypeFromInstanceOf(String varName, ASTNode instanceOf) {
+ return "Object";
+ }
+
+ /**
+ * Gets the type of a missing variable from a variable declaration expression.
+ * @param varName name of the missing variable
+ * @param varDeclaration expression closest to error
+ * @return the type of the missing variable
+ */
+ private String getTypeFromVarDeclaration(String varName, ASTNode varDeclaration) {
+ VariableDeclarationFragment declaration = (VariableDeclarationFragment) varDeclaration;
+ return declaration.resolveBinding().getType().getName();
+ }
+
+ /**
+ * Gets the type of a missing variable from a method invocation expression.
+ * @param varName name of the missing variable
+ * @param methodInvocation expression closest to error
+ * @return the type of the missing variable
+ */
+ private String getTypeFromMethodInvocation(String varName, ASTNode methodInvocation) {
+ MethodInvocation invocation = (MethodInvocation) methodInvocation;
+ List requiredParamTypes = Arrays.stream(
+ invocation.resolveMethodBinding().getParameterTypes()
+ ).map(ITypeBinding::getName).collect(Collectors.toList());
+ List providedParams = ((List>) invocation.arguments()).stream()
+ .map(Object::toString).collect(Collectors.toList());
+
+ int paramIndex = providedParams.indexOf(varName);
+
+ return paramIndex >= 0 ? requiredParamTypes.get(paramIndex) : "Object";
+ }
+
+ /**
+ * Gets the type of a missing variable from an array creation expression.
+ * @param varName name of the missing variable
+ * @param arrayCreation expression closest to error
+ * @return the type of the missing variable
+ */
+ private String getTypeFromArrayCreation(String varName, ASTNode arrayCreation) {
+ return "int";
+ }
+
+ /**
+ * Gets the type of a missing variable from an array access expression.
+ * @param varName name of the missing variable
+ * @param arrayAccess expression closest to error
+ * @return the type of the missing variable
+ */
+ private String getTypeFromArrayAccess(String varName, ASTNode arrayAccess) {
+ return "int";
+ }
+
+ /**
+ * Gets the type of a missing variable from an array initializer expression.
+ * @param varName name of the missing variable
+ * @param arrayInitializer expression closest to error
+ * @return the type of the missing variable
+ */
+ private String getTypeFromArrayInitializer(String varName, ASTNode arrayInitializer) {
+ ArrayInitializer initializer = (ArrayInitializer) arrayInitializer;
+ return initializer.resolveTypeBinding().getElementType().getName();
+ }
+
+ /**
+ * Gets the type of a missing variable from a cast expression.
+ * @param varName name of the missing variable
+ * @param castExpression expression closest to error
+ * @return the type of the missing variable
+ */
+ private String getTypeFromCastExpression(String varName, ASTNode castExpression) {
+ CastExpression cast = (CastExpression) castExpression;
+ return cast.getType().toString();
+ }
+
+ /**
+ * Gets the type of a missing variable from an assignment expression.
+ * @param varName name of the missing variable
+ * @param assignmentExpression expression closest to error
+ * @return the type of the missing variable
+ */
+ private String getTypeFromAssignment(String varName, ASTNode assignmentExpression) {
+ Assignment assignment = (Assignment) assignmentExpression;
+ return assignment.resolveTypeBinding().getName();
+ }
+
+ /**
+ * Checks if a token could be a type.
+ * @param token the token to check
+ * @return whether this token could be a type
+ */
+ private boolean couldBeType(String token) {
+ char[] chars = token.toCharArray();
+ return IntStream.range(0, chars.length).allMatch(
+ index -> Character.isJavaIdentifierPart(chars[index])
+ );
+ }
+
+}
diff --git a/java/src/processing/mode/java/preproc/PdePreprocessor.java b/java/src/processing/mode/java/preproc/PdePreprocessor.java
index c11b229667..1e4efca198 100644
--- a/java/src/processing/mode/java/preproc/PdePreprocessor.java
+++ b/java/src/processing/mode/java/preproc/PdePreprocessor.java
@@ -134,6 +134,7 @@
* what each type of file is for.
*
*/
+
public class PdePreprocessor {
protected static final String UNICODE_ESCAPES = "0123456789abcdefABCDEF";
@@ -998,6 +999,7 @@ private String write(final String program, final PrintWriter stream)
// be viewed usefully with Mozilla or IE
if (Preferences.getBoolean("preproc.output_parse_tree")) {
writeParseTree("parseTree.xml", parserAST);
+
}
return className;