| Index: Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ContextTrackingValidationCheck.java
|
| diff --git a/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ContextTrackingValidationCheck.java b/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ContextTrackingValidationCheck.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..ce1c3c2dcf61fd312b3cf634e59157810a575ffd
|
| --- /dev/null
|
| +++ b/Source/devtools/scripts/jsdoc-validator/src/org/chromium/devtools/jsdoc/checks/ContextTrackingValidationCheck.java
|
| @@ -0,0 +1,201 @@
|
| +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.Comment;
|
| +import com.google.javascript.rhino.head.ast.FunctionNode;
|
| +
|
| +import org.chromium.devtools.jsdoc.ValidationCheck;
|
| +import org.chromium.devtools.jsdoc.ValidatorContext;
|
| +import org.chromium.devtools.jsdoc.checks.TypeRecord.InheritanceEntry;
|
| +
|
| +import java.util.ArrayList;
|
| +import java.util.Collections;
|
| +import java.util.List;
|
| +import java.util.regex.Matcher;
|
| +import java.util.regex.Pattern;
|
| +
|
| +public class ContextTrackingValidationCheck extends ValidationCheck {
|
| +
|
| + private static final Pattern EXTENDS_PATTERN =
|
| + Pattern.compile("@extends\\s+\\{\\s*([^\\s}]+)\\s*\\}");
|
| + private static final Pattern RETURN_PATTERN =
|
| + Pattern.compile("@return\\s+\\{\\s*(.+)\\s*\\}");
|
| + private ContextTrackingState state;
|
| + private final List<ContextTrackingChecker> clients = new ArrayList<>(5);
|
| +
|
| + @Override
|
| + protected void setContext(ValidatorContext context) {
|
| + super.setContext(context);
|
| + state = new ContextTrackingState(context);
|
| + registerClient(new ProtoFollowsExtendsChecker());
|
| + registerClient(new RequiredThisAnnotationChecker());
|
| + registerClient(new ReturnAnnotationChecker());
|
| + }
|
| +
|
| + @Override
|
| + public void doVisit(AstNode node) {
|
| + switch (node.getType()) {
|
| + case Token.ASSIGN:
|
| + enterAssignNode((Assignment) node);
|
| + break;
|
| + case Token.FUNCTION:
|
| + enterFunctionNode((FunctionNode) node);
|
| + break;
|
| + default:
|
| + break;
|
| + }
|
| +
|
| + enterNode(node);
|
| + }
|
| +
|
| + @Override
|
| + public void didVisit(AstNode node) {
|
| + leaveNode(node);
|
| +
|
| + switch (node.getType()) {
|
| + case Token.ASSIGN:
|
| + leaveAssignNode((Assignment) node);
|
| + break;
|
| + case Token.FUNCTION:
|
| + leaveFunctionNode((FunctionNode) node);
|
| + break;
|
| + default:
|
| + break;
|
| + }
|
| + }
|
| +
|
| + public void registerClient(ContextTrackingChecker client) {
|
| + this.clients.add(client);
|
| + client.setState(state);
|
| + }
|
| +
|
| + private void enterNode(AstNode node) {
|
| + for (ContextTrackingChecker client : clients) {
|
| + client.enterNode(node);
|
| + }
|
| + }
|
| +
|
| + private void leaveNode(AstNode node) {
|
| + for (ContextTrackingChecker client : clients) {
|
| + client.leaveNode(node);
|
| + }
|
| + }
|
| +
|
| + private void enterFunctionNode(FunctionNode node) {
|
| + Comment jsDocNode = getJsDocNode(node);
|
| + AstNode nameNode = AstUtil.getFunctionNameNode(node);
|
| +
|
| + // It can be a type declaration: /** @constructor */ function MyType() {...}.
|
| + boolean isConstructor =
|
| + nameNode != null && rememberTypeRecordIfNeeded(getNodeText(nameNode), jsDocNode);
|
| + TypeRecord parentType = state.getCurrentFunctionRecord() == null
|
| + ? state.getCurrentTypeRecord()
|
| + : null;
|
| + state.pushFunctionRecord(new FunctionRecord(
|
| + node,
|
| + isConstructor,
|
| + getReturnType(jsDocNode),
|
| + parentType,
|
| + state.getCurrentFunctionRecord()));
|
| + }
|
| +
|
| + @SuppressWarnings("unused")
|
| + private void leaveFunctionNode(FunctionNode node) {
|
| + state.functionRecords.removeLast();
|
| + }
|
| +
|
| + private String getReturnType(Comment jsDocNode) {
|
| + if (jsDocNode == null) {
|
| + return null;
|
| + }
|
| + String jsDoc = getNodeText(jsDocNode);
|
| + Matcher m = RETURN_PATTERN.matcher(jsDoc);
|
| + if (!m.find()) {
|
| + return null;
|
| + }
|
| + return m.group(1);
|
| + }
|
| +
|
| + private void enterAssignNode(Assignment assignment) {
|
| + String assignedTypeName = getAssignedTypeName(assignment);
|
| + if (assignedTypeName == null) {
|
| + return;
|
| + }
|
| + if (AstUtil.isPrototypeName(assignedTypeName)) {
|
| + // MyType.prototype = ...
|
| + String typeName = AstUtil.getTypeNameFromPrototype(assignedTypeName);
|
| + TypeRecord typeRecord = state.typeRecordsByTypeName.get(typeName);
|
| + // We should push anything here to maintain a valid current type record.
|
| + state.pushTypeRecord(typeRecord);
|
| + state.pushFunctionRecord(null);
|
| + return;
|
| + }
|
| +
|
| + if (assignment.getRight().getType() == Token.FUNCTION) {
|
| + // MyType = function() {...}
|
| + rememberTypeRecordIfNeeded(assignedTypeName, getJsDocNode(assignment));
|
| + }
|
| +
|
| + }
|
| +
|
| + private void leaveAssignNode(Assignment assignment) {
|
| + String assignedTypeName = getAssignedTypeName(assignment);
|
| + if (assignedTypeName == null) {
|
| + return;
|
| + }
|
| + if (AstUtil.isPrototypeName(assignedTypeName)) {
|
| + // Remove the current type record when leaving prototype object.
|
| + state.typeRecords.removeLast();
|
| + state.functionRecords.removeLast();
|
| + return;
|
| + }
|
| + }
|
| +
|
| + private String getAssignedTypeName(Assignment assignment) {
|
| + AstNode node = AstUtil.getAssignedTypeNameNode(assignment);
|
| + return getNodeText(node);
|
| + }
|
| +
|
| + private boolean rememberTypeRecordIfNeeded(String typeName, Comment jsDocNode) {
|
| + String jsDoc = getNodeText(jsDocNode);
|
| + if (!isConstructor(jsDoc) && !isInterface(jsDoc)) {
|
| + return false;
|
| + }
|
| + TypeRecord record = new TypeRecord(
|
| + typeName,
|
| + isInterface(jsDoc),
|
| + getExtendsEntries(jsDocNode));
|
| + state.typeRecordsByTypeName.put(typeName, record);
|
| + return true;
|
| + }
|
| +
|
| + private static boolean isInterface(String jsDoc) {
|
| + return jsDoc != null && jsDoc.contains("@interface");
|
| + }
|
| +
|
| + private static boolean isConstructor(String jsDoc) {
|
| + return jsDoc != null && jsDoc.contains("@constructor");
|
| + }
|
| +
|
| + private static Comment getJsDocNode(AstNode node) {
|
| + if (node.getType() == Token.FUNCTION) {
|
| + return AstUtil.getJsDocNode((FunctionNode) node);
|
| + }
|
| + return node.getJsDocNode();
|
| + }
|
| +
|
| + private List<InheritanceEntry> getExtendsEntries(Comment jsDocNode) {
|
| + if (jsDocNode == null) {
|
| + return Collections.emptyList();
|
| + }
|
| + List<InheritanceEntry> result = new ArrayList<>(2);
|
| + Matcher matcher = EXTENDS_PATTERN.matcher(getNodeText(jsDocNode));
|
| + while (matcher.find()) {
|
| + result.add(new InheritanceEntry(matcher.group(1), jsDocNode, matcher.start(1)));
|
| + }
|
| +
|
| + return result;
|
| + }
|
| +}
|
|
|