blob: 6dfe8e3e05a5bcfe798f0bd3617f6307f7ca8b08 [file] [log] [blame]
/*******************************************************************************
* 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;
}
}