| /******************************************************************************* |
| * Copyright (c) 2010 xored software, Inc. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * xored software, Inc. - initial API and Implementation (Alex Panchenko) |
| *******************************************************************************/ |
| |
| package org.eclipse.dltk.internal.javascript.validation; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.dltk.core.builder.IBuildContext; |
| import org.eclipse.dltk.core.builder.IBuildParticipant; |
| import org.eclipse.dltk.internal.javascript.ti.JSDocSupport; |
| import org.eclipse.dltk.javascript.ast.AbstractNavigationVisitor; |
| import org.eclipse.dltk.javascript.ast.BreakStatement; |
| import org.eclipse.dltk.javascript.ast.CaseClause; |
| import org.eclipse.dltk.javascript.ast.CatchClause; |
| import org.eclipse.dltk.javascript.ast.ContinueStatement; |
| import org.eclipse.dltk.javascript.ast.DefaultClause; |
| import org.eclipse.dltk.javascript.ast.DoWhileStatement; |
| import org.eclipse.dltk.javascript.ast.Expression; |
| import org.eclipse.dltk.javascript.ast.ForEachInStatement; |
| import org.eclipse.dltk.javascript.ast.ForInStatement; |
| import org.eclipse.dltk.javascript.ast.ForStatement; |
| import org.eclipse.dltk.javascript.ast.FunctionStatement; |
| import org.eclipse.dltk.javascript.ast.IfStatement; |
| import org.eclipse.dltk.javascript.ast.Method; |
| import org.eclipse.dltk.javascript.ast.ReturnStatement; |
| import org.eclipse.dltk.javascript.ast.Script; |
| import org.eclipse.dltk.javascript.ast.Statement; |
| import org.eclipse.dltk.javascript.ast.StatementBlock; |
| import org.eclipse.dltk.javascript.ast.SwitchComponent; |
| import org.eclipse.dltk.javascript.ast.SwitchStatement; |
| import org.eclipse.dltk.javascript.ast.ThrowStatement; |
| import org.eclipse.dltk.javascript.ast.TryStatement; |
| import org.eclipse.dltk.javascript.ast.VoidExpression; |
| import org.eclipse.dltk.javascript.ast.WhileStatement; |
| import org.eclipse.dltk.javascript.core.JavaScriptProblems; |
| import org.eclipse.dltk.javascript.parser.Reporter; |
| import org.eclipse.dltk.javascript.parser.jsdoc.JSDocTag; |
| import org.eclipse.dltk.javascript.parser.jsdoc.JSDocTags; |
| import org.eclipse.osgi.util.NLS; |
| |
| public class FlowValidation extends AbstractNavigationVisitor<FlowStatus> |
| implements IBuildParticipant { |
| |
| private Reporter reporter; |
| private FlowScope scope; |
| |
| public void build(IBuildContext context) throws CoreException { |
| final Script script = JavaScriptValidations.parse(context); |
| if (script == null) { |
| return; |
| } |
| reporter = JavaScriptValidations.createReporter(context); |
| scope = new FlowScope(); |
| visit(script); |
| } |
| |
| @Override |
| public FlowStatus visitReturnStatement(ReturnStatement node) { |
| final FlowEndKind kind = node.getValue() != null ? FlowEndKind.RETURNS_VALUE |
| : FlowEndKind.RETURNS; |
| if (scope.add(kind) && scope.size() > 1) { |
| reporter.setMessage(JavaScriptProblems.RETURN_INCONSISTENT, |
| "return statement is inconsistent with previous usage"); |
| reporter.setRange(node.sourceStart(), node.sourceEnd()); |
| reporter.report(); |
| } |
| final FlowStatus status = new FlowStatus(); |
| if (node.getValue() == null) { |
| status.returnWithoutValue = true; |
| } else { |
| status.returnValue = true; |
| } |
| return status; |
| } |
| |
| @Override |
| public FlowStatus visitThrowStatement(ThrowStatement node) { |
| if (node.getException() != null) { |
| visit(node.getException()); |
| } |
| final FlowStatus status = new FlowStatus(); |
| status.returnThrow = true; |
| return status; |
| } |
| |
| @Override |
| public FlowStatus visitStatementBlock(StatementBlock node) { |
| return visitStatements(node.getStatements(), false); |
| } |
| |
| private FlowStatus visitStatements(final List<Statement> statements, |
| boolean isSwitch) { |
| FlowStatus status = new FlowStatus(); |
| status.noReturn = true; |
| int startRange = Integer.MAX_VALUE; |
| int endRange = -1; |
| boolean firstBreak = true; |
| for (Statement statement : statements) { |
| if (isFunctionDeclaration(statement)) { |
| visit(statement); |
| } else if (status.isTerminated()) { |
| if (isSwitch && statement instanceof BreakStatement |
| && firstBreak && status.isReturned()) { |
| firstBreak = false; |
| continue; |
| } |
| if (startRange > statement.sourceStart()) |
| startRange = statement.sourceStart(); |
| if (endRange < statement.sourceEnd()) |
| endRange = statement.sourceEnd(); |
| |
| } else { |
| status.add(visit(statement)); |
| } |
| } |
| if (startRange != Integer.MAX_VALUE) { |
| reporter.setMessage(JavaScriptProblems.UNREACHABLE_CODE, |
| "unreachable code"); |
| reporter.setRange(startRange, endRange); |
| reporter.report(); |
| } |
| return status; |
| } |
| |
| private boolean isFunctionDeclaration(Statement statement) { |
| if (statement instanceof VoidExpression) { |
| final Expression expression = ((VoidExpression) statement) |
| .getExpression(); |
| return expression instanceof FunctionStatement |
| && ((FunctionStatement) expression).isDeclaration(); |
| } else { |
| return false; |
| } |
| } |
| |
| @Override |
| public FlowStatus visitIfStatement(IfStatement node) { |
| |
| FlowStatus status = new FlowStatus(); |
| status.noReturn = true; |
| |
| if (node.getThenStatement() != null) { |
| FlowStatus thenFlow = visit(node.getThenStatement()); |
| if (thenFlow != null) { |
| status.noReturn = thenFlow.noReturn; |
| status.returnValue = thenFlow.returnValue; |
| status.returnWithoutValue = thenFlow.returnWithoutValue; |
| } |
| } |
| if (node.getElseStatement() != null) { |
| status.addBranch(visit(node.getElseStatement())); |
| } else { |
| status.noReturn = true; |
| } |
| |
| return status; |
| } |
| |
| private static FlowStatus clearBreak(final FlowStatus status) { |
| if (status != null) { |
| status.isBreak = false; |
| } |
| return status; |
| } |
| |
| @Override |
| public FlowStatus visitForStatement(ForStatement node) { |
| return clearBreak(super.visitForStatement(node)); |
| } |
| |
| @Override |
| public FlowStatus visitForInStatement(ForInStatement node) { |
| return clearBreak(super.visitForInStatement(node)); |
| } |
| |
| @Override |
| public FlowStatus visitForEachInStatement(ForEachInStatement node) { |
| return clearBreak(super.visitForEachInStatement(node)); |
| } |
| |
| @Override |
| public FlowStatus visitWhileStatement(WhileStatement node) { |
| return clearBreak(super.visitWhileStatement(node)); |
| } |
| |
| @Override |
| public FlowStatus visitDoWhileStatement(DoWhileStatement node) { |
| return clearBreak(super.visitDoWhileStatement(node)); |
| } |
| |
| @Override |
| public FlowStatus visitFunctionStatement(FunctionStatement node) { |
| final FlowScope savedScope = scope; |
| scope = new FlowScope(); |
| try { |
| final FlowStatus result = super.visitFunctionStatement(node); |
| if (scope.contains(FlowEndKind.RETURNS_VALUE) |
| && (scope.contains(FlowEndKind.RETURNS) || result.noReturn)) { |
| if (result.noReturn && result.returnValue |
| && node.getDocumentation() != null) { |
| JSDocTags parse = JSDocSupport.parse(node |
| .getDocumentation()); |
| // if it does return a value and it has a no return and it |
| // is a constructor function then don't report it. Very |
| // likely a construct to support a constructor function |
| // without the new keyword. |
| if (parse.count(JSDocTag.CONSTRUCTOR) == 1) |
| return result; |
| } |
| reportInconsistentReturn(node); |
| } |
| return result; |
| } finally { |
| scope = savedScope; |
| } |
| } |
| |
| @Override |
| protected FlowStatus visitMethod(Method method) { |
| final FlowScope savedScope = scope; |
| scope = new FlowScope(); |
| try { |
| return super.visitMethod(method); |
| } finally { |
| scope = savedScope; |
| } |
| } |
| |
| protected void reportInconsistentReturn(FunctionStatement node) { |
| reportInconsistentReturn(reporter, node); |
| } |
| |
| public static void reportInconsistentReturn(final Reporter reporter, |
| FunctionStatement node) { |
| reporter.setMessage( |
| JavaScriptProblems.FUNCTION_NOT_ALWAYS_RETURN_VALUE, |
| node.getName() != null ? NLS.bind( |
| "function {0} does not always return a value", node |
| .getName().getName()) |
| : "anonymous function does not always return a value"); |
| reporter.setRange(node.getBody().getRC(), node.getBody().getRC() + 1); |
| reporter.report(); |
| } |
| |
| @Override |
| public FlowStatus visitBreakStatement(BreakStatement node) { |
| final FlowStatus status = new FlowStatus(); |
| status.isBreak = true; |
| return status; |
| } |
| |
| @Override |
| public FlowStatus visitContinueStatement(ContinueStatement node) { |
| final FlowStatus status = new FlowStatus(); |
| status.isBreak = true; |
| return status; |
| } |
| |
| @Override |
| public FlowStatus visitTryStatement(TryStatement node) { |
| final FlowStatus status = new FlowStatus(); |
| final FlowStatus body = visit(node.getBody()); |
| status.add(body); |
| if (!node.getCatches().isEmpty()) { |
| status.returnThrow = false; |
| } |
| for (CatchClause catchClause : node.getCatches()) { |
| final Statement catchStatement = catchClause.getStatement(); |
| if (catchStatement != null) { |
| final FlowStatus c = visit(catchStatement); |
| if (!c.isReturned()) { |
| status.add(c); |
| } |
| } |
| } |
| if (node.getFinally() != null) { |
| final Statement finallyStatement = node.getFinally().getStatement(); |
| if (finallyStatement != null) { |
| final FlowStatus f = visit(finallyStatement); |
| if (f.isReturned()) { |
| status.add(f); |
| } |
| } |
| } |
| return status; |
| } |
| |
| @Override |
| public FlowStatus visitSwitchStatement(SwitchStatement node) { |
| final List<FlowStatus> statuses = new ArrayList<FlowStatus>(); |
| FlowStatus defaultClause = null; |
| if (node.getCondition() != null) |
| visit(node.getCondition()); |
| for (SwitchComponent component : node.getCaseClauses()) { |
| if (component instanceof CaseClause) { |
| final CaseClause caseClause = (CaseClause) component; |
| if (caseClause.getCondition() != null) { |
| visit(caseClause.getCondition()); |
| } |
| } |
| final FlowStatus s = visitStatements(component.getStatements(), |
| true); |
| if (component instanceof DefaultClause) { |
| defaultClause = s; |
| } else { |
| statuses.add(s); |
| } |
| } |
| if (defaultClause == null) { |
| defaultClause = new FlowStatus(); |
| defaultClause.noReturn = true; |
| } |
| boolean noReturn = false; |
| final FlowStatus status = new FlowStatus(); |
| for (FlowStatus s : statuses) { |
| status.addCase(s); |
| if (s.isReturned()) { |
| noReturn |= status.noReturn; |
| status.noReturn = false; |
| } else if (s.isBreak || s.isAnyReturn()) { |
| status.noReturn |= s.noReturn; |
| } |
| } |
| status.addBranch(defaultClause); |
| status.noReturn |= noReturn; |
| // TODO in a case statement a labeled break? |
| status.isBreak = false; |
| return status; |
| } |
| } |