Upstream version 5.34.98.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / scripts / jsdoc-validator / src / org / chromium / devtools / jsdoc / checks / ProtoFollowsExtendsAnnotationCheck.java
1 package org.chromium.devtools.jsdoc.checks;
2
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;
9
10 import java.util.ArrayDeque;
11 import java.util.Deque;
12 import java.util.HashMap;
13 import java.util.Map;
14 import java.util.regex.Matcher;
15 import java.util.regex.Pattern;
16
17 public final class ProtoFollowsExtendsAnnotationCheck extends ValidationCheck {
18
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*\\}");
23
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<>();
27
28     @Override
29     public void doVisit(AstNode node) {
30         if (node.getType() == Token.ASSIGN) {
31             handleAssignNode((Assignment) node);
32             return;
33         }
34         if (node.getType() == Token.FUNCTION) {
35             handleFunctionNode((FunctionNode) node);
36             return;
37         }
38         if (node.getType() == Token.OBJECTLIT) {
39             objectLiteralStack.push(node);
40             return;
41         }
42         if (node.getType() == Token.COLON) {
43             handleColonNode((ObjectProperty) node);
44             return;
45         }
46     }
47
48     @Override
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()));
54         }
55     }
56
57     private void handleFunctionNode(FunctionNode node) {
58         Comment jsDocNode = getJsDocNode(node);
59         if (jsDocNode == null) {
60             return;
61         }
62         AstNode nameNode = AstUtil.getFunctionNameNode(node);
63         if (nameNode == null) {
64             return;
65         }
66         String functionTypeName = getContext().getNodeText(nameNode);
67         rememberExtendedTypeIfNeeded(functionTypeName, jsDocNode);
68     }
69
70     private void handleColonNode(ObjectProperty node) {
71         if (objectLiteralStack.isEmpty()) {
72             return;
73         }
74         String propertyName = getContext().getNodeText(node.getLeft());
75         if (!PROTO_PROPERTY_NAME.equals(propertyName)) {
76             return;
77         }
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");
82             return;
83         }
84         String currentPrototype = objectLiteralToPrototypeName.get(objectLiteralStack.peek());
85         if (currentPrototype == null) {
86             // FIXME: __proto__: Foo.prototype not in an object literal for Bar.prototype.
87             return;
88         }
89         String currentType = getTypeNameFromPrototype(currentPrototype);
90         String superType = getTypeNameFromPrototype(value);
91         ExtendsEntry entry = typeNameToExtendsEntry.remove(currentType);
92         if (entry == null) {
93             getContext().reportErrorInNode(node.getRight(), 0, String.format(
94                     "No @extends annotation for %s extending %s", currentType, superType));
95             return;
96         }
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));
102         }
103     }
104
105     private String getTypeNameFromPrototype(String value) {
106         return value.substring(0, value.length() - PROTOTYPE_SUFFIX.length());
107     }
108
109     private void handleAssignNode(Assignment assignment) {
110         AstNode typeNameNode = assignment.getLeft();
111         if (typeNameNode.getType() != Token.GETPROP && typeNameNode.getType() != Token.NAME) {
112             return;
113         }
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);
119             } else {
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));
126                 }
127             }
128             return;
129         }
130
131         if (assignment.getRight().getType() != Token.FUNCTION) {
132             return;
133         }
134
135         Comment jsDocNode = getJsDocNode(assignment);
136         if (jsDocNode != null) {
137             rememberExtendedTypeIfNeeded(typeName, jsDocNode);
138         }
139     }
140
141     private void rememberExtendedTypeIfNeeded(String typeName, Comment jsDocNode) {
142         final ExtendsEntry extendsEntry = getExtendsEntry(jsDocNode);
143         if (extendsEntry == null) {
144             return;
145         }
146         typeNameToExtendsEntry.put(typeName, extendsEntry);
147     }
148
149     private ExtendsEntry getExtendsEntry(Comment jsDocNode) {
150         String jsDoc = getContext().getNodeText(jsDocNode);
151         if (!jsDoc.contains("@constructor")) {
152             return null;
153         }
154         Matcher matcher = EXTENDS_PATTERN.matcher(jsDoc);
155         if (!matcher.find()) {
156             return null;
157         }
158
159         return new ExtendsEntry(matcher.group(1), matcher.start(1), jsDocNode);
160     }
161
162     @Override
163     public void didVisit(AstNode node) {
164         if (node.getType() == Token.OBJECTLIT) {
165             objectLiteralStack.pop();
166             objectLiteralToPrototypeName.remove(node);
167         }
168     }
169
170     private Comment getJsDocNode(AstNode node) {
171         return node.getJsDocNode();
172     }
173
174     private static class ExtendsEntry {
175         public final String extendedType;
176         public final int offsetInNodeText;
177         public final Comment jsDocNode;
178
179         public ExtendsEntry(String extendedType, int offsetInNodeText, Comment jsDocNode) {
180             this.extendedType = extendedType;
181             this.offsetInNodeText = offsetInNodeText;
182             this.jsDocNode = jsDocNode;
183         }
184     }
185 }