| 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..7a968c5b9c5b4f1bdbf387fc1d70eeff30bdbf17
|
| --- /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 final Set<TypeRecord> typesWithAssignedProto = new HashSet<>();
|
| +
|
| + @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) {
|
| + 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)));
|
| + }
|
| + }
|
| +}
|
|
|