1 package org.chromium.devtools.jsdoc.checks;
3 import com.google.javascript.rhino.head.Token;
4 import com.google.javascript.rhino.head.ast.Assignment;
5 import com.google.javascript.rhino.head.ast.AstNode;
6 import com.google.javascript.rhino.head.ast.Comment;
7 import com.google.javascript.rhino.head.ast.FunctionNode;
8 import com.google.javascript.rhino.head.ast.ObjectProperty;
10 import java.util.ArrayDeque;
11 import java.util.Deque;
12 import java.util.HashMap;
14 import java.util.regex.Matcher;
15 import java.util.regex.Pattern;
17 public final class ProtoFollowsExtendsAnnotationCheck extends ValidationCheck {
19 private static final String PROTO_PROPERTY_NAME = "__proto__";
20 private static final String PROTOTYPE_SUFFIX = ".prototype";
21 private static final Pattern EXTENDS_PATTERN =
22 Pattern.compile("@extends\\s+\\{\\s*([^\\s}]+)\\s*\\}");
24 private final Map<String, ExtendsEntry> typeNameToExtendsEntry = new HashMap<>();
25 private final Deque<AstNode> objectLiteralStack = new ArrayDeque<>();
26 private final Map<AstNode, String> objectLiteralToPrototypeName = new HashMap<>();
29 public void doVisit(AstNode node) {
30 if (node.getType() == Token.ASSIGN) {
31 handleAssignNode((Assignment) node);
34 if (node.getType() == Token.FUNCTION) {
35 handleFunctionNode((FunctionNode) node);
38 if (node.getType() == Token.OBJECTLIT) {
39 objectLiteralStack.push(node);
42 if (node.getType() == Token.COLON) {
43 handleColonNode((ObjectProperty) node);
49 public void didTraverseTree() {
50 for (Map.Entry<String, ExtendsEntry> e : typeNameToExtendsEntry.entrySet()) {
51 ExtendsEntry entry = e.getValue();
52 getContext().reportErrorInNode(entry.jsDocNode, entry.offsetInNodeText,
53 String.format("No __proto__ assigned for type %s having @extends", e.getKey()));
57 private void handleFunctionNode(FunctionNode node) {
58 Comment jsDocNode = getJsDocNode(node);
59 if (jsDocNode == null) {
62 AstNode nameNode = AstUtil.getFunctionNameNode(node);
63 if (nameNode == null) {
66 String functionTypeName = getContext().getNodeText(nameNode);
67 rememberExtendedTypeIfNeeded(functionTypeName, jsDocNode);
70 private void handleColonNode(ObjectProperty node) {
71 if (objectLiteralStack.isEmpty()) {
74 String propertyName = getContext().getNodeText(node.getLeft());
75 if (!PROTO_PROPERTY_NAME.equals(propertyName)) {
78 String value = getContext().getNodeText(node.getRight());
79 if (!value.endsWith(PROTOTYPE_SUFFIX)) {
80 getContext().reportErrorInNode(
81 node.getRight(), 0, "__proto__ value is not a prototype");
84 String currentPrototype = objectLiteralToPrototypeName.get(objectLiteralStack.peek());
85 if (currentPrototype == null) {
86 // FIXME: __proto__: Foo.prototype not in an object literal for Bar.prototype.
89 String currentType = getTypeNameFromPrototype(currentPrototype);
90 String superType = getTypeNameFromPrototype(value);
91 ExtendsEntry entry = typeNameToExtendsEntry.remove(currentType);
93 getContext().reportErrorInNode(node.getRight(), 0, String.format(
94 "No @extends annotation for %s extending %s", currentType, superType));
97 if (!superType.equals(entry.extendedType)) {
98 getContext().reportErrorInNode(node.getRight(), 0, String.format(
99 "Supertype does not match %s declared in @extends for %s (line %d)",
100 entry.extendedType, currentType,
101 getContext().getPosition(entry.jsDocNode, entry.offsetInNodeText).line));
105 private String getTypeNameFromPrototype(String value) {
106 return value.substring(0, value.length() - PROTOTYPE_SUFFIX.length());
109 private void handleAssignNode(Assignment assignment) {
110 AstNode typeNameNode = assignment.getLeft();
111 if (typeNameNode.getType() != Token.GETPROP && typeNameNode.getType() != Token.NAME) {
114 String typeName = getContext().getNodeText(typeNameNode);
115 if (typeName.endsWith(PROTOTYPE_SUFFIX)) {
116 AstNode prototypeValueNode = assignment.getRight();
117 if (prototypeValueNode.getType() == Token.OBJECTLIT) {
118 objectLiteralToPrototypeName.put(assignment.getRight(), typeName);
120 typeName = getTypeNameFromPrototype(typeName);
121 ExtendsEntry extendsEntry = typeNameToExtendsEntry.get(typeName);
122 if (extendsEntry != null) {
123 getContext().reportErrorInNode(prototypeValueNode, 0, String.format(
124 "@extends found for type %s but its prototype is not an object "
125 + "containing __proto__", typeName));
131 if (assignment.getRight().getType() != Token.FUNCTION) {
135 Comment jsDocNode = getJsDocNode(assignment);
136 if (jsDocNode != null) {
137 rememberExtendedTypeIfNeeded(typeName, jsDocNode);
141 private void rememberExtendedTypeIfNeeded(String typeName, Comment jsDocNode) {
142 final ExtendsEntry extendsEntry = getExtendsEntry(jsDocNode);
143 if (extendsEntry == null) {
146 typeNameToExtendsEntry.put(typeName, extendsEntry);
149 private ExtendsEntry getExtendsEntry(Comment jsDocNode) {
150 String jsDoc = getContext().getNodeText(jsDocNode);
151 if (!jsDoc.contains("@constructor")) {
154 Matcher matcher = EXTENDS_PATTERN.matcher(jsDoc);
155 if (!matcher.find()) {
159 return new ExtendsEntry(matcher.group(1), matcher.start(1), jsDocNode);
163 public void didVisit(AstNode node) {
164 if (node.getType() == Token.OBJECTLIT) {
165 objectLiteralStack.pop();
166 objectLiteralToPrototypeName.remove(node);
170 private Comment getJsDocNode(AstNode node) {
171 return node.getJsDocNode();
174 private static class ExtendsEntry {
175 public final String extendedType;
176 public final int offsetInNodeText;
177 public final Comment jsDocNode;
179 public ExtendsEntry(String extendedType, int offsetInNodeText, Comment jsDocNode) {
180 this.extendedType = extendedType;
181 this.offsetInNodeText = offsetInNodeText;
182 this.jsDocNode = jsDocNode;