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;
9 import org.chromium.devtools.jsdoc.ValidationCheck;
10 import org.chromium.devtools.jsdoc.ValidatorContext;
11 import org.chromium.devtools.jsdoc.checks.TypeRecord.InheritanceEntry;
13 import java.util.ArrayList;
14 import java.util.Collections;
15 import java.util.List;
16 import java.util.regex.Matcher;
17 import java.util.regex.Pattern;
19 public class ContextTrackingValidationCheck extends ValidationCheck {
21 private static final Pattern EXTENDS_PATTERN =
22 Pattern.compile("@extends\\s+\\{\\s*([^\\s}]+)\\s*\\}");
23 private static final Pattern RETURN_PATTERN =
24 Pattern.compile("@return\\s+\\{\\s*(.+)\\s*\\}");
25 private ContextTrackingState state;
26 private final List<ContextTrackingChecker> clients = new ArrayList<>(5);
29 protected void setContext(ValidatorContext context) {
30 super.setContext(context);
31 state = new ContextTrackingState(context);
32 registerClient(new ProtoFollowsExtendsChecker());
33 registerClient(new ReturnAnnotationChecker());
34 registerClient(new FunctionReceiverChecker());
38 public void doVisit(AstNode node) {
39 switch (node.getType()) {
41 enterAssignNode((Assignment) node);
44 enterFunctionNode((FunctionNode) node);
54 public void didVisit(AstNode node) {
57 switch (node.getType()) {
59 leaveAssignNode((Assignment) node);
62 leaveFunctionNode((FunctionNode) node);
69 public void registerClient(ContextTrackingChecker client) {
70 this.clients.add(client);
71 client.setState(state);
74 private void enterNode(AstNode node) {
75 for (ContextTrackingChecker client : clients) {
76 client.enterNode(node);
80 private void leaveNode(AstNode node) {
81 for (ContextTrackingChecker client : clients) {
82 client.leaveNode(node);
86 private void enterFunctionNode(FunctionNode node) {
87 Comment jsDocNode = getJsDocNode(node);
88 AstNode nameNode = AstUtil.getFunctionNameNode(node);
90 // It can be a type declaration: /** @constructor */ function MyType() {...}.
91 String functionName = getNodeText(nameNode);
92 boolean isConstructor =
93 functionName != null && rememberTypeRecordIfNeeded(functionName, jsDocNode);
94 TypeRecord parentType = state.getCurrentFunctionRecord() == null
95 ? state.getCurrentTypeRecord()
97 state.pushFunctionRecord(new FunctionRecord(
101 getReturnType(jsDocNode),
103 state.getCurrentFunctionRecord()));
106 @SuppressWarnings("unused")
107 private void leaveFunctionNode(FunctionNode node) {
108 state.functionRecords.removeLast();
111 private String getReturnType(Comment jsDocNode) {
112 if (jsDocNode == null) {
115 String jsDoc = getNodeText(jsDocNode);
116 Matcher m = RETURN_PATTERN.matcher(jsDoc);
123 private void enterAssignNode(Assignment assignment) {
124 String assignedTypeName = getAssignedTypeName(assignment);
125 if (assignedTypeName == null) {
128 if (AstUtil.isPrototypeName(assignedTypeName)) {
129 // MyType.prototype = ...
130 String typeName = AstUtil.getTypeNameFromPrototype(assignedTypeName);
131 TypeRecord typeRecord = state.typeRecordsByTypeName.get(typeName);
132 // We should push anything here to maintain a valid current type record.
133 state.pushTypeRecord(typeRecord);
134 state.pushFunctionRecord(null);
138 if (assignment.getRight().getType() == Token.FUNCTION) {
139 // MyType = function() {...}
140 rememberTypeRecordIfNeeded(assignedTypeName, getJsDocNode(assignment));
145 private void leaveAssignNode(Assignment assignment) {
146 String assignedTypeName = getAssignedTypeName(assignment);
147 if (assignedTypeName == null) {
150 if (AstUtil.isPrototypeName(assignedTypeName)) {
151 // Remove the current type record when leaving prototype object.
152 state.typeRecords.removeLast();
153 state.functionRecords.removeLast();
158 private String getAssignedTypeName(Assignment assignment) {
159 AstNode node = AstUtil.getAssignedTypeNameNode(assignment);
160 return getNodeText(node);
163 private boolean rememberTypeRecordIfNeeded(String typeName, Comment jsDocNode) {
164 String jsDoc = getNodeText(jsDocNode);
165 if (!isConstructor(jsDoc) && !isInterface(jsDoc)) {
168 TypeRecord record = new TypeRecord(
171 getExtendsEntries(jsDocNode));
172 state.typeRecordsByTypeName.put(typeName, record);
176 private static boolean isInterface(String jsDoc) {
177 return jsDoc != null && jsDoc.contains("@interface");
180 private static boolean isConstructor(String jsDoc) {
181 return jsDoc != null && jsDoc.contains("@constructor");
184 private static Comment getJsDocNode(AstNode node) {
185 if (node.getType() == Token.FUNCTION) {
186 return AstUtil.getJsDocNode((FunctionNode) node);
188 return node.getJsDocNode();
191 private List<InheritanceEntry> getExtendsEntries(Comment jsDocNode) {
192 if (jsDocNode == null) {
193 return Collections.emptyList();
195 List<InheritanceEntry> result = new ArrayList<>(2);
196 Matcher matcher = EXTENDS_PATTERN.matcher(getNodeText(jsDocNode));
197 while (matcher.find()) {
198 result.add(new InheritanceEntry(matcher.group(1), jsDocNode, matcher.start(1)));