Commit 0c925d9c authored by apavlov@chromium.org's avatar apavlov@chromium.org

DevTools: [JsDocValidator] Be smarter about function name and JSDoc detection

R=aandrey, sergeyv
NOTRY=true

Review URL: https://codereview.chromium.org/319733002

git-svn-id: svn://svn.chromium.org/blink/trunk@175648 bbb929c8-8fbe-4397-9dbb-9b2b20218538
parent 83a3a9bf
2b7a3211b34c415d5dd8b0fe6e4cb9e3b46365de62c11059e600059a4bf2f864 src b5c1c0e9e16e027abae9f603ad6ff9228fe3b634be5b170ff9c267e8a3c54573 src
8690d7b1e6486f73ee34ff076af215901e944d17293dc9f039d014d2f94b5804 jsdoc-validator.jar ac033f55138d8536bd031ca2a1f0cfc703ffa80dbfd24b2bd34dbfda1b90ff5c jsdoc-validator.jar
...@@ -24,21 +24,23 @@ public class AstUtil { ...@@ -24,21 +24,23 @@ public class AstUtil {
} }
AstNode parentNode = functionNode.getParent(); AstNode parentNode = functionNode.getParent();
if (parentNode == null) { while (parentNode != null) {
return null;
}
switch (parentNode.getType()) { switch (parentNode.getType()) {
case Token.COLON: case Token.COLON:
return ((ObjectProperty) parentNode).getLeft(); return ((ObjectProperty) parentNode).getLeft();
case Token.ASSIGN: case Token.ASSIGN:
Assignment assignment = (Assignment) parentNode; Assignment assignment = (Assignment) parentNode;
if (assignment.getRight() == functionNode) { if (assignment.getRight() == functionNode &&
assignment.getLeft().getType() == Token.NAME) {
return assignment.getLeft(); return assignment.getLeft();
} }
parentNode = assignment.getParent();
break; break;
case Token.VAR: case Token.VAR:
return ((VariableInitializer) parentNode).getTarget(); return ((VariableInitializer) parentNode).getTarget();
default:
return null;
}
} }
return null; return null;
...@@ -76,21 +78,23 @@ public class AstUtil { ...@@ -76,21 +78,23 @@ public class AstUtil {
// reader.onloadend = function() {...} // reader.onloadend = function() {...}
AstNode parentNode = functionNode.getParent(); AstNode parentNode = functionNode.getParent();
if (parentNode == null) { while (parentNode != null) {
return null;
}
switch (parentNode.getType()) { switch (parentNode.getType()) {
case Token.COLON: case Token.COLON:
return ((ObjectProperty) parentNode).getLeft().getJsDocNode(); return ((ObjectProperty) parentNode).getLeft().getJsDocNode();
case Token.ASSIGN: case Token.ASSIGN:
Assignment assignment = (Assignment) parentNode; Assignment assignment = (Assignment) parentNode;
if (assignment.getRight() == functionNode) { if (assignment.getJsDocNode() != null) {
return assignment.getJsDocNode(); return assignment.getJsDocNode();
} else {
parentNode = assignment.getParent();
} }
break; break;
case Token.VAR: case Token.VAR:
return parentNode.getParent().getJsDocNode(); return parentNode.getParent().getJsDocNode();
default:
return null;
}
} }
return null; return null;
......
...@@ -27,8 +27,7 @@ abstract class ContextTrackingChecker { ...@@ -27,8 +27,7 @@ abstract class ContextTrackingChecker {
return state.getContext(); return state.getContext();
} }
protected boolean hasAnnotationTag(FunctionNode node, String tagName) { protected boolean hasAnnotationTag(Comment comment, String tagName) {
Comment comment = AstUtil.getJsDocNode(node);
return comment != null && return comment != null &&
Pattern.matches("(?s).*@" + tagName + "\\b.*", getContext().getNodeText(comment)); Pattern.matches("(?s).*@" + tagName + "\\b.*", getContext().getNodeText(comment));
} }
......
...@@ -88,14 +88,16 @@ public class ContextTrackingValidationCheck extends ValidationCheck { ...@@ -88,14 +88,16 @@ public class ContextTrackingValidationCheck extends ValidationCheck {
AstNode nameNode = AstUtil.getFunctionNameNode(node); AstNode nameNode = AstUtil.getFunctionNameNode(node);
// It can be a type declaration: /** @constructor */ function MyType() {...}. // It can be a type declaration: /** @constructor */ function MyType() {...}.
String functionName = getNodeText(nameNode); // Or even /** @constructor */ window.Foo.Bar = function() {...}.
boolean isConstructor = String functionName = nameNode == null ? null : getNodeText(nameNode);
functionName != null && rememberTypeRecordIfNeeded(functionName, jsDocNode); boolean isConstructor = rememberTypeRecordIfNeeded(functionName, jsDocNode);
TypeRecord parentType = state.getCurrentFunctionRecord() == null TypeRecord parentType = state.getCurrentFunctionRecord() == null
? state.getCurrentTypeRecord() ? state.getCurrentTypeRecord()
: null; : null;
state.pushFunctionRecord(new FunctionRecord( state.pushFunctionRecord(new FunctionRecord(
node, node,
AstUtil.getJsDocNode(node),
functionName, functionName,
isConstructor, isConstructor,
getReturnType(jsDocNode), getReturnType(jsDocNode),
...@@ -162,6 +164,9 @@ public class ContextTrackingValidationCheck extends ValidationCheck { ...@@ -162,6 +164,9 @@ public class ContextTrackingValidationCheck extends ValidationCheck {
private boolean rememberTypeRecordIfNeeded(String typeName, Comment jsDocNode) { private boolean rememberTypeRecordIfNeeded(String typeName, Comment jsDocNode) {
String jsDoc = getNodeText(jsDocNode); String jsDoc = getNodeText(jsDocNode);
if (typeName == null) {
return isConstructor(jsDoc) || isInterface(jsDoc);
}
if (!isConstructor(jsDoc) && !isInterface(jsDoc)) { if (!isConstructor(jsDoc) && !isInterface(jsDoc)) {
return false; return false;
} }
......
...@@ -190,23 +190,29 @@ public final class FunctionReceiverChecker extends ContextTrackingChecker { ...@@ -190,23 +190,29 @@ public final class FunctionReceiverChecker extends ContextTrackingChecker {
private void checkThisAnnotation(FunctionRecord function) { private void checkThisAnnotation(FunctionRecord function) {
AstNode functionNameNode = AstUtil.getFunctionNameNode(function.functionNode); AstNode functionNameNode = AstUtil.getFunctionNameNode(function.functionNode);
if (functionNameNode == null) { if (functionNameNode == null && function.jsDocNode == null) {
// Do not check anonymous functions without a JSDoc.
return; return;
} }
boolean hasThisAnnotation = hasAnnotationTag(function.functionNode, "this"); AstNode errorTargetNode =
functionNameNode == null ? function.jsDocNode : functionNameNode;
if (errorTargetNode == null) {
errorTargetNode = function.functionNode;
}
boolean hasThisAnnotation = hasAnnotationTag(function.jsDocNode, "this");
if (hasThisAnnotation == functionReferencesThis(function)) { if (hasThisAnnotation == functionReferencesThis(function)) {
return; return;
} }
if (hasThisAnnotation) { if (hasThisAnnotation) {
if (!function.isTopLevelFunction()) { if (!function.isTopLevelFunction()) {
reportErrorAtNodeStart( reportErrorAtNodeStart(
functionNameNode, errorTargetNode,
"@this annotation found for function not referencing 'this'"); "@this annotation found for function not referencing 'this'");
} }
return; return;
} else { } else {
reportErrorAtNodeStart( reportErrorAtNodeStart(
functionNameNode, errorTargetNode,
"@this annotation is required for functions referencing 'this'"); "@this annotation is required for functions referencing 'this'");
} }
} }
...@@ -237,7 +243,7 @@ public final class FunctionReceiverChecker extends ContextTrackingChecker { ...@@ -237,7 +243,7 @@ public final class FunctionReceiverChecker extends ContextTrackingChecker {
private void processFunctionUsesAsArgument( private void processFunctionUsesAsArgument(
FunctionRecord function, Set<SymbolicArgument> argumentUses) { FunctionRecord function, Set<SymbolicArgument> argumentUses) {
if (argumentUses == null || if (argumentUses == null ||
hasAnnotationTag(function.functionNode, "suppressReceiverCheck")) { hasAnnotationTag(function.jsDocNode, "suppressReceiverCheck")) {
return; return;
} }
......
package org.chromium.devtools.jsdoc.checks; package org.chromium.devtools.jsdoc.checks;
import com.google.javascript.rhino.head.ast.Comment;
import com.google.javascript.rhino.head.ast.FunctionNode; import com.google.javascript.rhino.head.ast.FunctionNode;
public class FunctionRecord { public class FunctionRecord {
final FunctionNode functionNode; final FunctionNode functionNode;
final Comment jsDocNode;
final String name; final String name;
final boolean isConstructor; final boolean isConstructor;
final String returnType; final String returnType;
final TypeRecord enclosingType; final TypeRecord enclosingType;
final FunctionRecord enclosingFunctionRecord; final FunctionRecord enclosingFunctionRecord;
public FunctionRecord(FunctionNode functionNode, String name, boolean isConstructor, public FunctionRecord(FunctionNode functionNode, Comment jsDocNode, String name,
String returnType, TypeRecord parentType, FunctionRecord enclosingFunctionRecord) { boolean isConstructor, String returnType, TypeRecord parentType,
FunctionRecord enclosingFunctionRecord) {
this.functionNode = functionNode; this.functionNode = functionNode;
this.jsDocNode = jsDocNode;
this.name = name; this.name = name;
this.isConstructor = isConstructor; this.isConstructor = isConstructor;
this.returnType = returnType; this.returnType = returnType;
......
...@@ -17,10 +17,10 @@ import java.util.regex.Pattern; ...@@ -17,10 +17,10 @@ import java.util.regex.Pattern;
public final class MethodAnnotationChecker extends ContextTrackingChecker { public final class MethodAnnotationChecker extends ContextTrackingChecker {
private static final Pattern PARAM_PATTERN = private static final Pattern PARAM_PATTERN =
Pattern.compile("@param\\s+(\\{.+\\}\\s+)?([^\\s]+).*$", Pattern.MULTILINE); Pattern.compile("^[^@\n]*@param\\s+(\\{.+\\}\\s+)?([^\\s]+).*$", Pattern.MULTILINE);
private static final Pattern INVALID_RETURN_PATTERN = private static final Pattern INVALID_RETURN_PATTERN =
Pattern.compile("@return(?:s.*|\\s+[^{]*)$", Pattern.MULTILINE); Pattern.compile("^[^@\n]*(@)return(?:s.*|\\s+[^{]*)$", Pattern.MULTILINE);
private final Set<FunctionRecord> valueReturningFunctions = new HashSet<>(); private final Set<FunctionRecord> valueReturningFunctions = new HashSet<>();
private final Set<FunctionRecord> throwingFunctions = new HashSet<>(); private final Set<FunctionRecord> throwingFunctions = new HashSet<>();
...@@ -187,7 +187,7 @@ public final class MethodAnnotationChecker extends ContextTrackingChecker { ...@@ -187,7 +187,7 @@ public final class MethodAnnotationChecker extends ContextTrackingChecker {
return -1; return -1;
} }
Matcher m = INVALID_RETURN_PATTERN.matcher(jsDoc); Matcher m = INVALID_RETURN_PATTERN.matcher(jsDoc);
return m.find() ? m.start() : -1; return m.find() ? m.start(1) : -1;
} }
private static AstNode getFunctionNameNode(FunctionNode functionNode) { private static AstNode getFunctionNameNode(FunctionNode functionNode) {
......
...@@ -161,23 +161,29 @@ public final class ProtoFollowsExtendsChecker extends ContextTrackingChecker { ...@@ -161,23 +161,29 @@ public final class ProtoFollowsExtendsChecker extends ContextTrackingChecker {
} }
typesWithAssignedProto.add(currentType); typesWithAssignedProto.add(currentType);
String value = state.getNodeText(node.getRight()); String value = state.getNodeText(node.getRight());
if (!AstUtil.isPrototypeName(value)) { boolean isNullPrototype = "null".equals(value);
if (!isNullPrototype && !AstUtil.isPrototypeName(value)) {
reportErrorAtNodeStart( reportErrorAtNodeStart(
node.getRight(), "__proto__ value is not a prototype"); node.getRight(), "__proto__ value is not a prototype");
return; return;
} }
String superType = AstUtil.getTypeNameFromPrototype(value); String superType = isNullPrototype ? "null" : AstUtil.getTypeNameFromPrototype(value);
if (type.isInterface) { if (type.isInterface) {
reportErrorAtNodeStart(node.getLeft(), String.format( reportErrorAtNodeStart(node.getLeft(), String.format(
"__proto__ defined for interface %s", type.typeName)); "__proto__ defined for interface %s", type.typeName));
return; return;
} else { } else {
if (type.extendedTypes.isEmpty()) { if (!isNullPrototype && type.extendedTypes.isEmpty()) {
reportErrorAtNodeStart(node.getRight(), String.format( reportErrorAtNodeStart(node.getRight(), String.format(
"No @extends annotation for %s extending %s", type.typeName, superType)); "No @extends annotation for %s extending %s", type.typeName, superType));
return; return;
} }
} }
if (isNullPrototype) {
return;
}
// FIXME: Should we check that there is only one @extend-ed type // FIXME: Should we check that there is only one @extend-ed type
// for the non-interface |type|? Closure is supposed to do this anyway... // for the non-interface |type|? Closure is supposed to do this anyway...
InheritanceEntry entry = type.getFirstExtendedType(); InheritanceEntry entry = type.getFirstExtendedType();
......
...@@ -84,6 +84,23 @@ TypeOne.prototype = { ...@@ -84,6 +84,23 @@ TypeOne.prototype = {
_privateMethod: function() // OK - non-public method. _privateMethod: function() // OK - non-public method.
{ {
var obj = {};
/**
* @constructor
* @param {number} val
*/
obj["a"] = obj["b"] = function(val) { // OK - constructor
this.foo = val;
}
/**
* @param {number} val
*/
obj["c"] = obj["d"] = function(val) { // ERROR - no @this
this.foo = val;
}
return 1; return 1;
}, },
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment