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;