Chromium Code Reviews| Index: Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ProtoFollowsExtendsChecker.java |
| diff --git a/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ProtoFollowsExtendsChecker.java b/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ProtoFollowsExtendsChecker.java |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..e8bcb24629ec14cb3ff31d32772d7395b92ac493 |
| --- /dev/null |
| +++ b/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ProtoFollowsExtendsChecker.java |
| @@ -0,0 +1,126 @@ |
| +package org.chromium.devtools.jsdoc.checks; |
| + |
| +import com.google.javascript.rhino.head.Token; |
| +import com.google.javascript.rhino.head.ast.Assignment; |
| +import com.google.javascript.rhino.head.ast.AstNode; |
| +import com.google.javascript.rhino.head.ast.ObjectProperty; |
| + |
| +import org.chromium.devtools.jsdoc.checks.TypeRecord.InheritanceEntry; |
| + |
| +import java.util.HashSet; |
| +import java.util.Set; |
| + |
| +public final class ProtoFollowsExtendsChecker extends ContextTrackingChecker { |
| + |
| + private static final String PROTO_PROPERTY_NAME = "__proto__"; |
| + private Set<TypeRecord> typesWithAssignedProto = new HashSet<>(); |
|
eustas
2014/01/28 08:47:21
final?
apavlov
2014/01/28 10:40:17
Done.
|
| + |
| + @Override |
| + protected void enterNode(AstNode node) { |
| + if (node.getType() == Token.COLON) { |
| + handleColonNode((ObjectProperty) node); |
| + return; |
| + } |
| + if (node.getType() == Token.ASSIGN) { |
| + handleAssignment((Assignment) node); |
| + return; |
| + } |
| + } |
| + |
| + @Override |
| + protected void leaveNode(AstNode node) { |
| + if (node.getType() == Token.SCRIPT) { |
| + checkFinished(); |
| + } |
| + } |
| + |
| + private void checkFinished() { |
| + for (TypeRecord record : getState().getTypeRecordsByTypeName().values()) { |
| + if (record.isInterface || typesWithAssignedProto.contains(record)) { |
| + continue; |
| + } |
| + InheritanceEntry entry = record.getFirstExtendedType(); |
| + if (entry != null) { |
| + getState().getContext().reportErrorInNode( |
| + entry.jsDocNode, entry.offsetInJsDocText, |
| + String.format("No __proto__ assigned for type %s having @extends", |
| + record.typeName)); |
| + } |
| + } |
| + } |
| + |
| + private void handleColonNode(ObjectProperty node) { |
| + ContextTrackingState state = getState(); |
| + TypeRecord type = state.getCurrentTypeRecord(); |
| + if (type == null) { |
| + return; |
| + } |
| + String propertyName = state.getNodeText(node.getLeft()); |
| + 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); |
| + String value = state.getNodeText(node.getRight()); |
| + if (!AstUtil.isPrototypeName(value)) { |
| + state.getContext().reportErrorInNode( |
| + node.getRight(), 0, "__proto__ value is not a prototype"); |
| + return; |
| + } |
| + String superType = AstUtil.getTypeNameFromPrototype(value); |
| + if (type.isInterface) { |
| + state.getContext().reportErrorInNode(node.getLeft(), 0, String.format( |
| + "__proto__ defined for interface %s", type.typeName)); |
| + return; |
| + } else { |
| + if (type.extendedTypes.isEmpty()) { |
| + state.getContext().reportErrorInNode(node.getRight(), 0, String.format( |
| + "No @extends annotation for %s extending %s", type.typeName, superType)); |
| + 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... |
| + InheritanceEntry entry = type.getFirstExtendedType(); |
| + String extendedTypeName = entry.superTypeName; |
| + if (!superType.equals(extendedTypeName)) { |
| + state.getContext().reportErrorInNode(node.getRight(), 0, String.format( |
| + "Supertype does not match %s declared in @extends for %s (line %d)", |
| + extendedTypeName, type.typeName, |
| + state.getContext().getPosition(entry.jsDocNode, entry.offsetInJsDocText).line)); |
| + } |
| + } |
| + |
| + private void handleAssignment(Assignment assignment) { |
| + String assignedTypeName = |
| + getState().getNodeText(AstUtil.getAssignedTypeNameNode(assignment)); |
| + if (assignedTypeName == null) { |
| + return; |
| + } |
| + if (!AstUtil.isPrototypeName(assignedTypeName)) { |
| + return; |
| + } |
| + AstNode prototypeValueNode = assignment.getRight(); |
| + |
| + if (prototypeValueNode.getType() == 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.extendedTypes.isEmpty()) { |
| + state.getContext().reportErrorInNode(prototypeValueNode, 0, String.format( |
| + "@extends found for type %s but its prototype is not an object " |
| + + "containing __proto__", AstUtil.getTypeNameFromPrototype(assignedTypeName))); |
| + } |
| + } |
| +} |