blob: 9605fbf40addd06903ae3edd5d51f7050910b5e0 [file] [log] [blame]
package org.chromium.devtools.jsdoc.checks;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class ProtoFollowsExtendsChecker extends ContextTrackingChecker {
private static final String PROTO_PROPERTY_NAME = "__proto__";
private static final Set<String> IGNORED_SUPER_TYPES = new HashSet<>();
static {
IGNORED_SUPER_TYPES.add("WebInspector.Object");
}
private final Set<TypeRecord> typesWithAssignedProto = new HashSet<>();
private final Set<FunctionRecord> functionsMissingSuperCall = new HashSet<>();
@Override
protected void enterNode(Node node) {
switch (node.getToken()) {
case ASSIGN:
case VAR:
handleAssignment(node);
break;
case STRING_KEY:
handleColonNode(node);
break;
case FUNCTION:
enterFunction();
break;
case CALL:
handleCall(node);
break;
default:
break;
}
}
private void handleCall(Node callNode) {
FunctionRecord contextFunction = getState().getCurrentFunctionRecord();
if (contextFunction == null || !contextFunction.isConstructor()
|| !functionsMissingSuperCall.contains(contextFunction)) {
return;
}
String typeName = validSuperConstructorName(callNode);
if (typeName == null) {
return;
}
TypeRecord typeRecord = getState().typeRecordsByTypeName.get(contextFunction.name);
if (typeRecord == null) {
return;
}
JSTypeExpression extendedType = typeRecord.getExtendedType();
// FIXME: Strip template parameters from the extendedType.
if (extendedType == null
|| !typeName.equals(AstUtil.getAnnotationTypeString(extendedType))) {
return;
}
functionsMissingSuperCall.remove(contextFunction);
}
private String validSuperConstructorName(Node callNode) {
String callTarget = getContext().getNodeText(callNode.getFirstChild());
int lastDotIndex = callTarget.lastIndexOf('.');
if (lastDotIndex == -1) {
return null;
}
String methodName = callTarget.substring(lastDotIndex + 1);
if (!"call".equals(methodName) && !"apply".equals(methodName)) {
return null;
}
List<Node> arguments = AstUtil.getArguments(callNode);
if (arguments.isEmpty() || !"this".equals(getContext().getNodeText(arguments.get(0)))) {
return null;
}
return callTarget.substring(0, lastDotIndex);
}
@Override
protected void leaveNode(Node node) {
if (node.getToken() == Token.SCRIPT) {
checkFinished();
return;
}
if (node.getToken() == Token.FUNCTION) {
leaveFunction();
return;
}
}
private void enterFunction() {
FunctionRecord function = getState().getCurrentFunctionRecord();
JSTypeExpression extendedType = getExtendedTypeToCheck(function);
if (extendedType == null) {
return;
}
if (!IGNORED_SUPER_TYPES.contains(AstUtil.getAnnotationTypeString(extendedType))) {
functionsMissingSuperCall.add(function);
}
}
private void leaveFunction() {
FunctionRecord function = getState().getCurrentFunctionRecord();
if (!functionsMissingSuperCall.contains(function)) {
return;
}
JSTypeExpression extendedType = getExtendedTypeToCheck(function);
if (extendedType == null) {
return;
}
String annotationTypeString = AstUtil.getAnnotationTypeString(extendedType);
if (annotationTypeString.startsWith("HTML")) {
return;
}
reportErrorAtNodeStart(AstUtil.getFunctionNameNode(function.functionNode),
String.format("Type %s extends %s but does not properly invoke its constructor",
function.name, annotationTypeString));
}
private JSTypeExpression getExtendedTypeToCheck(FunctionRecord function) {
if (!function.isConstructor() || function.name == null) {
return null;
}
TypeRecord type = getState().typeRecordsByTypeName.get(function.name);
if (type == null || type.isInterface()) {
return null;
}
return type.getExtendedType();
}
private void checkFinished() {
for (TypeRecord record : getState().getTypeRecordsByTypeName().values()) {
if (record.isInterface() || typesWithAssignedProto.contains(record)) {
continue;
}
JSTypeExpression extendedType = record.getExtendedType();
if (extendedType != null) {
Node rootNode = extendedType.getRoot();
if (rootNode.getToken() == Token.BANG && rootNode.getFirstChild() != null) {
rootNode = rootNode.getFirstChild();
}
getContext().reportErrorAtOffset(rootNode.getSourceOffset(),
String.format("No __proto__ assigned for type %s having @extends",
record.typeName));
}
}
}
private void handleColonNode(Node node) {
ContextTrackingState state = getState();
TypeRecord type = state.getCurrentTypeRecord();
if (type == null) {
return;
}
String propertyName = node.getString();
if (!PROTO_PROPERTY_NAME.equals(propertyName)) {
return;
}
TypeRecord currentType = state.getCurrentTypeRecord();
if (currentType == null) {
// FIXME: __proto__: Foo.prototype not in an object literal for Bar.prototype.
return;
}
typesWithAssignedProto.add(currentType);
Node rightNode = node.getFirstChild();
String value = state.getNodeText(rightNode);
boolean isNullPrototype = "null".equals(value);
if (!isNullPrototype && !AstUtil.isPrototypeName(value)) {
reportErrorAtNodeStart(rightNode, "__proto__ value is not a prototype");
return;
}
String superType = isNullPrototype ? "null" : AstUtil.getTypeNameFromPrototype(value);
if (type.isInterface()) {
reportErrorAtNodeStart(
node, String.format("__proto__ defined for interface %s", type.typeName));
return;
} else {
if (!isNullPrototype && type.getExtendedType() == null) {
reportErrorAtNodeStart(
rightNode, String.format("No @extends annotation for %s extending %s",
type.typeName, superType));
return;
}
}
if (isNullPrototype) {
return;
}
// FIXME: Should we check that there is only one @extend-ed type
// for the non-interface |type|? Closure is supposed to do this anyway...
JSTypeExpression extendedType = type.getExtendedType();
String extendedTypeName = AstUtil.getAnnotationTypeString(extendedType);
if (!superType.equals(extendedTypeName)) {
reportErrorAtNodeStart(rightNode,
String.format(
"Supertype does not match %s declared in @extends for %s (line %d)",
extendedTypeName, type.typeName,
state.getContext()
.getPosition(extendedType.getRoot().getSourceOffset())
.line));
}
}
private void handleAssignment(Node assignment) {
String assignedTypeName =
getState().getNodeText(AstUtil.getAssignedTypeNameNode(assignment));
if (assignedTypeName == null) {
return;
}
if (!AstUtil.isPrototypeName(assignedTypeName)) {
return;
}
Node prototypeValueNode = assignment.getLastChild();
if (prototypeValueNode.getToken() == Token.OBJECTLIT) {
return;
}
// Foo.prototype = notObjectLiteral
ContextTrackingState state = getState();
TypeRecord type = state.getCurrentTypeRecord();
if (type == null) {
// Assigning a prototype for unknown type. Leave it to the closure compiler.
return;
}
if (type.getExtendedType() != null) {
reportErrorAtNodeStart(prototypeValueNode,
String.format("@extends found for type %s but its prototype is not an object "
+ "containing __proto__",
AstUtil.getTypeNameFromPrototype(assignedTypeName)));
}
}
}