com.google.javascript.jscomp.CheckJSDoc Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler Show documentation
Show all versions of closure-compiler Show documentation
Closure Compiler is a JavaScript optimizing compiler. It parses your
JavaScript, analyzes it, removes dead code and rewrites and minimizes
what's left. It also checks syntax, variable references, and types, and
warns about common JavaScript pitfalls. It is used in many of Google's
JavaScript apps, including Gmail, Google Web Search, Google Maps, and
Google Docs.
/*
* Copyright 2015 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import static com.google.common.base.Preconditions.checkState;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import javax.annotation.Nullable;
/**
* Checks for misplaced, misused or deprecated JSDoc annotations.
*
* @author [email protected] (Chad Killingsworth)
*/
final class CheckJSDoc extends AbstractPostOrderCallback implements HotSwapCompilerPass {
public static final DiagnosticType MISPLACED_MSG_ANNOTATION =
DiagnosticType.disabled("JSC_MISPLACED_MSG_ANNOTATION",
"Misplaced message annotation. @desc, @hidden, and @meaning annotations should only "
+ "be on message nodes.");
public static final DiagnosticType MISPLACED_ANNOTATION =
DiagnosticType.warning("JSC_MISPLACED_ANNOTATION",
"Misplaced {0} annotation. {1}");
public static final DiagnosticType ANNOTATION_DEPRECATED =
DiagnosticType.warning("JSC_ANNOTATION_DEPRECATED",
"The {0} annotation is deprecated. {1}");
public static final DiagnosticType DISALLOWED_MEMBER_JSDOC =
DiagnosticType.warning("JSC_DISALLOWED_MEMBER_JSDOC",
"Class level JSDocs (@interface, @extends, etc.) are not allowed on class members");
static final DiagnosticType ARROW_FUNCTION_AS_CONSTRUCTOR = DiagnosticType.error(
"JSC_ARROW_FUNCTION_AS_CONSTRUCTOR",
"Arrow functions cannot be used as constructors");
static final DiagnosticType DEFAULT_PARAM_MUST_BE_MARKED_OPTIONAL = DiagnosticType.error(
"JSC_DEFAULT_PARAM_MUST_BE_MARKED_OPTIONAL",
"Inline JSDoc on default parameters must be marked as optional");
public static final DiagnosticType INVALID_NO_SIDE_EFFECT_ANNOTATION =
DiagnosticType.error(
"JSC_INVALID_NO_SIDE_EFFECT_ANNOTATION",
"@nosideeffects may only appear in externs files.");
public static final DiagnosticType INVALID_MODIFIES_ANNOTATION =
DiagnosticType.error(
"JSC_INVALID_MODIFIES_ANNOTATION", "@modifies may only appear in externs files.");
public static final DiagnosticType INVALID_DEFINE_ON_LET =
DiagnosticType.error(
"JSC_INVALID_DEFINE_ON_LET",
"variables annotated with @define may only be declared with VARs, ASSIGNs, or CONSTs");
private final AbstractCompiler compiler;
CheckJSDoc(AbstractCompiler compiler) {
this.compiler = compiler;
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverseEs6(compiler, externs, this);
NodeTraversal.traverseEs6(compiler, root, this);
}
@Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
NodeTraversal.traverseEs6(compiler, scriptRoot, this);
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
JSDocInfo info = n.getJSDocInfo();
validateTypeAnnotations(n, info);
validateFunctionJsDoc(n, info);
validateMsgJsDoc(n, info);
validateDeprecatedJsDoc(n, info);
validateNoCollapse(n, info);
validateClassLevelJsDoc(n, info);
validateArrowFunction(n);
validateDefaultValue(n, info);
validateTemplates(n, info);
validateTypedefs(n, info);
validateNoSideEffects(n, info);
validateAbstractJsDoc(n, info);
validateDefinesDeclaration(n, info);
}
private void validateTypedefs(Node n, JSDocInfo info) {
if (info != null && info.getTypedefType() != null && isClassDecl(n)) {
reportMisplaced(n, "typedef", "@typedef does not make sense on a class declaration.");
}
}
private void validateTemplates(Node n, JSDocInfo info) {
if (info != null
&& !info.getTemplateTypeNames().isEmpty()
&& !info.isConstructorOrInterface()
&& !isClassDecl(n)
&& !info.containsFunctionDeclaration()) {
if (getFunctionDecl(n) != null) {
reportMisplaced(n, "template",
"The template variable is unused."
+ " Please remove the @template annotation.");
} else {
reportMisplaced(n, "template",
"@template is only allowed in class, constructor, interface, function "
+ "or method declarations");
}
}
}
/**
* @return The function node associated with the function declaration associated with the
* specified node, no null if no such function exists.
*/
@Nullable
private Node getFunctionDecl(Node n) {
if (n.isFunction()) {
return n;
}
if (n.isMemberFunctionDef()) {
return n.getFirstChild();
}
if (NodeUtil.isNameDeclaration(n)
&& n.getFirstFirstChild() != null
&& n.getFirstFirstChild().isFunction()) {
return n.getFirstFirstChild();
}
if (n.isAssign() && n.getFirstChild().isQualifiedName() && n.getLastChild().isFunction()) {
return n.getLastChild();
}
if (n.isStringKey() && n.getGrandparent() != null
&& ClosureRewriteClass.isGoogDefineClass(n.getGrandparent())
&& n.getFirstChild().isFunction()) {
return n.getFirstChild();
}
if (n.isGetterDef() || n.isSetterDef()) {
return n.getFirstChild();
}
return null;
}
private boolean isClassDecl(Node n) {
return isClass(n)
|| (n.isAssign() && isClass(n.getLastChild()))
|| (NodeUtil.isNameDeclaration(n) && isNameInitializeWithClass(n.getFirstChild()))
|| isNameInitializeWithClass(n);
}
private boolean isNameInitializeWithClass(Node n) {
return n != null && n.isName() && n.hasChildren() && isClass(n.getFirstChild());
}
private boolean isClass(Node n) {
return n.isClass()
|| (n.isCall() && compiler.getCodingConvention().isClassFactoryCall(n));
}
/**
* Checks that class-level annotations like @interface/@extends are not used on member functions.
*/
private void validateClassLevelJsDoc(Node n, JSDocInfo info) {
if (info != null && n.isMemberFunctionDef()
&& hasClassLevelJsDoc(info)) {
report(n, DISALLOWED_MEMBER_JSDOC);
}
}
private void validateAbstractJsDoc(Node n, JSDocInfo info) {
if (info == null || !info.isAbstract()) {
return;
}
if (isClassDecl(n)) {
return;
}
Node functionNode = getFunctionDecl(n);
if (functionNode == null) {
// @abstract annotation on a non-function
report(
n,
MISPLACED_ANNOTATION,
"@abstract",
"only functions or non-static methods can be abstract");
return;
}
if (!info.isConstructor() && NodeUtil.getFunctionBody(functionNode).hasChildren()) {
// @abstract annotation on a function with a non-empty body
report(n, MISPLACED_ANNOTATION, "@abstract",
"function with a non-empty body cannot be abstract");
return;
}
if ((n.isMemberFunctionDef() || n.isStringKey()) && "constructor".equals(n.getString())) {
// @abstract annotation on an ES6 or goog.defineClass constructor
report(n, MISPLACED_ANNOTATION, "@abstract", "constructors cannot be abstract");
return;
}
if (!info.isConstructor()
&& !n.isMemberFunctionDef()
&& !n.isStringKey()
&& !n.isGetterDef()
&& !n.isSetterDef()
&& !NodeUtil.isPrototypeMethod(functionNode)) {
// @abstract annotation on a non-method (or static method) in ES5
report(
n,
MISPLACED_ANNOTATION,
"@abstract",
"only functions or non-static methods can be abstract");
return;
}
if (n.isStaticMember()) {
// @abstract annotation on a static method in ES6
report(n, MISPLACED_ANNOTATION, "@abstract", "static methods cannot be abstract");
return;
}
}
private boolean hasClassLevelJsDoc(JSDocInfo info) {
return info.isConstructorOrInterface()
|| info.hasBaseType()
|| info.getImplementedInterfaceCount() != 0
|| info.getExtendedInterfacesCount() != 0;
}
/**
* Checks that deprecated annotations such as @expose are not present
*/
private void validateDeprecatedJsDoc(Node n, JSDocInfo info) {
if (info != null && info.isExpose()) {
report(n, ANNOTATION_DEPRECATED, "@expose",
"Use @nocollapse or @export instead.");
}
}
/**
* Warns when nocollapse annotations are present on nodes
* which are not eligible for property collapsing.
*/
private void validateNoCollapse(Node n, JSDocInfo info) {
if (n.isFromExterns()) {
if (info != null && info.isNoCollapse()) {
// @nocollapse has no effect in externs
reportMisplaced(n, "nocollapse", "This JSDoc has no effect in externs.");
}
return;
}
if (!NodeUtil.isPrototypePropertyDeclaration(n.getParent())) {
return;
}
JSDocInfo jsdoc = n.getJSDocInfo();
if (jsdoc != null && jsdoc.isNoCollapse()) {
reportMisplaced(n, "nocollapse", "This JSDoc has no effect on prototype properties.");
}
}
/**
* Checks that JSDoc intended for a function is actually attached to a
* function.
*/
private void validateFunctionJsDoc(Node n, JSDocInfo info) {
if (info == null) {
return;
}
if (info.containsFunctionDeclaration() && !info.hasType()) {
// This JSDoc should be attached to a FUNCTION node, or an assignment
// with a function as the RHS, etc.
switch (n.getToken()) {
case FUNCTION:
case GETTER_DEF:
case SETTER_DEF:
case MEMBER_FUNCTION_DEF:
case STRING_KEY:
case COMPUTED_PROP:
case EXPORT:
return;
case GETELEM:
case GETPROP:
if (n.getFirstChild().isQualifiedName()) {
return;
}
break;
case VAR:
case LET:
case CONST:
case ASSIGN: {
Node lhs = n.getFirstChild();
Node rhs = NodeUtil.getRValueOfLValue(lhs);
if (rhs != null && isClass(rhs) && !info.isConstructor()) {
break;
}
// TODO(tbreisacher): Check that the RHS of the assignment is a
// function. Note that it can be a FUNCTION node, but it can also be
// a call to goog.abstractMethod, goog.functions.constant, etc.
return;
}
default:
break;
}
reportMisplaced(n,
"function", "This JSDoc is not attached to a function node. "
+ "Are you missing parentheses?");
}
}
/**
* Checks that annotations for messages ({@code @desc}, {@code @hidden},
* and {@code @meaning})
* are in the proper place, namely on names starting with MSG_ which
* indicates they should be
* extracted for translation. A later pass checks that the right side is
* a call to goog.getMsg.
*/
private void validateMsgJsDoc(Node n,
JSDocInfo info) {
if (info == null) {
return;
}
if (info.getDescription() != null || info.isHidden() || info.getMeaning() != null) {
boolean descOkay = false;
switch (n.getToken()) {
case ASSIGN:
case VAR:
case LET:
case CONST:
descOkay = isValidMsgName(n.getFirstChild());
break;
case STRING_KEY:
descOkay = isValidMsgName(n);
break;
case GETPROP:
if (n.isFromExterns() && n.isQualifiedName()) {
descOkay = isValidMsgName(n);
}
break;
default:
break;
}
if (!descOkay) {
report(n, MISPLACED_MSG_ANNOTATION);
}
}
}
/** Returns whether of not the given name is valid target for the result of goog.getMsg */
private boolean isValidMsgName(Node nameNode) {
if (nameNode.isName() || nameNode.isStringKey()) {
return nameNode.getString().startsWith("MSG_");
} else {
checkState(nameNode.isQualifiedName());
return nameNode.getLastChild().getString().startsWith("MSG_");
}
}
/**
* Check that JSDoc with a {@code @type} annotation is in a valid place.
*/
private void validateTypeAnnotations(Node n, JSDocInfo info) {
if (info != null && info.hasType()) {
boolean valid = false;
switch (n.getToken()) {
// Function declarations are valid
case FUNCTION:
valid = NodeUtil.isFunctionDeclaration(n);
break;
// Object literal properties, catch declarations and variable
// initializers are valid.
case NAME:
case DEFAULT_VALUE:
case ARRAY_PATTERN:
case OBJECT_PATTERN:
Node parent = n.getParent();
switch (parent.getToken()) {
case GETTER_DEF:
case SETTER_DEF:
case CATCH:
case FUNCTION:
case VAR:
case LET:
case CONST:
case PARAM_LIST:
valid = true;
break;
default:
break;
}
break;
// Casts, variable declarations, exports, and Object literal properties are valid.
case CAST:
case VAR:
case LET:
case CONST:
case EXPORT:
case STRING_KEY:
case GETTER_DEF:
case SETTER_DEF:
valid = true;
break;
// Property assignments are valid, if at the root of an expression.
case ASSIGN: {
Node lvalue = n.getFirstChild();
valid = n.getParent().isExprResult()
&& (lvalue.isGetProp()
|| lvalue.isGetElem()
|| lvalue.matchesQualifiedName("exports"));
break;
}
case GETPROP:
valid = n.getParent().isExprResult() && n.isQualifiedName();
break;
case CALL:
valid = info.isDefine();
break;
default:
break;
}
if (!valid) {
reportMisplaced(n, "type", "Type annotations are not allowed here. "
+ "Are you missing parentheses?");
}
}
}
private void reportMisplaced(Node n, String annotationName, String note) {
compiler.report(JSError.make(n, MISPLACED_ANNOTATION,
annotationName, note));
}
private void report(Node n, DiagnosticType type, String... arguments) {
compiler.report(JSError.make(n, type, arguments));
}
/**
* Check that an arrow function is not annotated with {@constructor}.
*/
private void validateArrowFunction(Node n) {
if (n.isArrowFunction()) {
JSDocInfo info = NodeUtil.getBestJSDocInfo(n);
if (info != null && info.isConstructorOrInterface()) {
report(n, ARROW_FUNCTION_AS_CONSTRUCTOR);
}
}
}
/**
* Check that an arrow function is not annotated with {@constructor}.
*/
private void validateDefaultValue(Node n, JSDocInfo info) {
if (n.isDefaultValue() && n.getParent().isParamList() && info != null) {
JSTypeExpression typeExpr = info.getType();
if (typeExpr == null) {
return;
}
Node typeNode = typeExpr.getRoot();
if (typeNode.getToken() != Token.EQUALS) {
report(typeNode, DEFAULT_PARAM_MUST_BE_MARKED_OPTIONAL);
}
}
}
/**
* Check that @nosideeeffects annotations are only present in externs.
*/
private void validateNoSideEffects(Node n, JSDocInfo info) {
// Cannot have @modifies or @nosideeffects in regular (non externs) js. Report errors.
if (info == null) {
return;
}
if (n.isFromExterns()) {
return;
}
if (info.hasSideEffectsArgumentsAnnotation() || info.modifiesThis()) {
report(n, INVALID_MODIFIES_ANNOTATION);
}
if (info.isNoSideEffects()) {
report(n, INVALID_NO_SIDE_EFFECT_ANNOTATION);
}
}
/**
* Check that a let declaration is not used with {@defines}
*/
private void validateDefinesDeclaration(Node n, JSDocInfo info) {
if (info != null && info.isDefine() && n.isLet()) {
report(n, INVALID_DEFINE_ON_LET);
}
}
}