com.google.javascript.jscomp.lint.CheckJSDocStyle Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com.liferay.frontend.js.minifier
Show all versions of com.liferay.frontend.js.minifier
Liferay Frontend JS Minifier
/*
* 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.lint;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList;
import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.CompilerPass;
import com.google.javascript.jscomp.DiagnosticGroup;
import com.google.javascript.jscomp.DiagnosticType;
import com.google.javascript.jscomp.ExportTestFunctions;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.AbstractPreOrderCallback;
import com.google.javascript.jscomp.NodeUtil;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import java.util.List;
import javax.annotation.Nullable;
/**
* Checks for various JSDoc-related style issues, such as function definitions without JsDoc, params
* with no corresponding {@code @param} annotation, coding conventions not being respected, etc.
*/
public final class CheckJSDocStyle extends AbstractPostOrderCallback implements CompilerPass {
public static final DiagnosticType CLASS_DISALLOWED_JSDOC =
DiagnosticType.disabled("JSC_CLASS_DISALLOWED_JSDOC",
"@constructor annotations are redundant on classes.");
public static final DiagnosticType MISSING_JSDOC =
DiagnosticType.disabled("JSC_MISSING_JSDOC", "Function must have JSDoc.");
public static final DiagnosticType INCORRECT_ANNOTATION_ON_GETTER_SETTER =
DiagnosticType.disabled(
"JSC_TYPE_ON_GETTER_SETTER",
"Getters and setters must not have @type annotations. Did you mean @return or @param"
+ " instead?");
public static final DiagnosticType MISSING_PARAMETER_JSDOC =
DiagnosticType.disabled("JSC_MISSING_PARAMETER_JSDOC", "Parameter must have JSDoc.{0}");
public static final DiagnosticType MIXED_PARAM_JSDOC_STYLES =
DiagnosticType.disabled("JSC_MIXED_PARAM_JSDOC_STYLES",
"Functions may not use both @param annotations and inline JSDoc");
public static final DiagnosticType MISSING_RETURN_JSDOC =
DiagnosticType.disabled(
"JSC_MISSING_RETURN_JSDOC",
"Function that returns a value must have JSDoc indicating the return type.{0}");
public static final DiagnosticType OPTIONAL_PARAM_NOT_MARKED_OPTIONAL =
DiagnosticType.disabled(
"JSC_OPTIONAL_PARAM_NOT_MARKED_OPTIONAL",
"Parameter {0} is optional so it must have a JSDoc type ending with ''=''");
public static final DiagnosticType WRONG_NUMBER_OF_PARAMS =
DiagnosticType.disabled("JSC_WRONG_NUMBER_OF_PARAMS", "Wrong number of @param annotations");
public static final DiagnosticType INCORRECT_PARAM_NAME =
DiagnosticType.disabled("JSC_INCORRECT_PARAM_NAME",
"Incorrect param name. Are your @param annotations in the wrong order?");
public static final DiagnosticType EXTERNS_FILES_SHOULD_BE_ANNOTATED =
DiagnosticType.disabled("JSC_EXTERNS_FILES_SHOULD_BE_ANNOTATED",
"Externs files should be annotated with @externs in the @fileoverview block.");
public static final DiagnosticType PREFER_BACKTICKS_TO_AT_SIGN_CODE =
DiagnosticType.disabled(
"JSC_PREFER_BACKTICKS_TO_AT_SIGN_CODE",
"Use `some_code` instead of '{'@code some_code'}'.");
public static final DiagnosticGroup LINT_DIAGNOSTICS =
new DiagnosticGroup(
CLASS_DISALLOWED_JSDOC,
MISSING_JSDOC,
INCORRECT_ANNOTATION_ON_GETTER_SETTER,
MISSING_PARAMETER_JSDOC,
MIXED_PARAM_JSDOC_STYLES,
MISSING_RETURN_JSDOC,
OPTIONAL_PARAM_NOT_MARKED_OPTIONAL,
WRONG_NUMBER_OF_PARAMS,
INCORRECT_PARAM_NAME,
EXTERNS_FILES_SHOULD_BE_ANNOTATED,
PREFER_BACKTICKS_TO_AT_SIGN_CODE);
public static final DiagnosticGroup ALL_DIAGNOSTICS = new DiagnosticGroup(LINT_DIAGNOSTICS);
private final AbstractCompiler compiler;
public CheckJSDocStyle(AbstractCompiler compiler) {
this.compiler = compiler;
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverse(compiler, root, this);
NodeTraversal.traverse(compiler, externs, new ExternsCallback());
}
@Override
public void visit(NodeTraversal t, Node n, Node unused) {
switch (n.getToken()) {
case FUNCTION:
visitFunction(t, n);
break;
case CLASS:
visitClass(t, n);
break;
case ASSIGN:
checkStyleForPrivateProperties(t, n);
break;
case VAR:
case LET:
case CONST:
case STRING_KEY:
case SCRIPT:
break;
case MEMBER_FUNCTION_DEF:
case GETTER_DEF:
case SETTER_DEF:
// Don't need to call visitFunction because this JSDoc will be visited when the function is
// visited.
if (NodeUtil.getEnclosingClass(n) != null) {
checkStyleForPrivateProperties(t, n);
}
break;
default:
visitNonFunction(t, n);
}
}
private void checkForAtSignCodePresence(NodeTraversal t, Node n, @Nullable JSDocInfo jsDoc) {
if (jsDoc == null) {
return;
}
if (jsDoc.isAtSignCodePresent()) {
t.report(n, PREFER_BACKTICKS_TO_AT_SIGN_CODE);
}
}
private void visitNonFunction(NodeTraversal t, Node n) {
JSDocInfo jsDoc = n.getJSDocInfo();
checkForAtSignCodePresence(t, n, jsDoc);
}
private void checkStyleForPrivateProperties(NodeTraversal t, Node n) {
JSDocInfo jsDoc = NodeUtil.getBestJSDocInfo(n);
checkForAtSignCodePresence(t, n, jsDoc);
}
private static void checkNoTypeOnGettersAndSetters(
NodeTraversal t, Node function, JSDocInfo jsDoc) {
if (function.getGrandparent().isClassMembers()) {
Node memberNode = function.getParent();
if (memberNode.isSetterDef() || memberNode.isGetterDef()) {
if (jsDoc != null && jsDoc.hasType()) {
t.report(function, INCORRECT_ANNOTATION_ON_GETTER_SETTER);
}
}
}
}
private void visitFunction(NodeTraversal t, Node function) {
JSDocInfo jsDoc = NodeUtil.getBestJSDocInfo(function);
checkForAtSignCodePresence(t, function, jsDoc);
if (jsDoc == null && !hasAnyInlineJsDoc(function)) {
checkMissingJsDoc(t, function);
} else {
if (t.inGlobalScope()
|| hasAnyInlineJsDoc(function)
|| !jsDoc.getParameterNames().isEmpty()
|| jsDoc.hasReturnType()
|| jsDoc.isOverride()) {
checkParams(t, function, jsDoc);
}
checkNoTypeOnGettersAndSetters(t, function, jsDoc);
checkReturn(function, jsDoc);
}
}
private void visitClass(NodeTraversal t, Node cls) {
JSDocInfo jsDoc = NodeUtil.getBestJSDocInfo(cls);
checkForAtSignCodePresence(t, cls, jsDoc);
if (jsDoc == null) {
return;
}
if (jsDoc.isConstructor()) {
t.report(cls, CLASS_DISALLOWED_JSDOC);
}
}
private void checkMissingJsDoc(NodeTraversal t, Node function) {
if (isFunctionThatShouldHaveJsDoc(t, function) && !isTestMethod(function)) {
t.report(function, MISSING_JSDOC);
}
}
/**
* Whether the given function should have JSDoc. True if it's a function declared
* in the global scope, or a method on a class which is declared in the global scope.
*/
private boolean isFunctionThatShouldHaveJsDoc(NodeTraversal t, Node function) {
if (!(t.inGlobalHoistScope() || t.inModuleScope())) {
// TODO(b/233631820): this should check for the module hoist scope instead
return false;
}
if (NodeUtil.isFunctionDeclaration(function)) {
return true;
}
if (NodeUtil.isNameDeclaration(function.getGrandparent()) || function.getParent().isAssign()) {
return true;
}
if (function.getParent().isExport()) {
return true;
}
if (function.getGrandparent().isClassMembers()) {
Node memberNode = function.getParent();
if (memberNode.isMemberFunctionDef()) {
// A constructor with no parameters doesn't need JSDoc,
// but all other member functions do.
return !isConstructorWithoutParameters(function);
} else if (memberNode.isGetterDef() || memberNode.isSetterDef()) {
return true;
}
}
return function.getGrandparent().isObjectLit()
&& NodeUtil.isCallTo(function.getGrandparent().getParent(), "Polymer");
}
/** Whether this is a test method (test* or setup/teardown) that does not require JSDoc */
private boolean isTestMethod(Node function) {
Node bestLValue = NodeUtil.getBestLValue(function);
String name = bestLValue != null ? NodeUtil.getBestLValueName(bestLValue) : null;
return name != null && ExportTestFunctions.isTestFunction(name);
}
private boolean isConstructorWithoutParameters(Node function) {
return NodeUtil.isEs6Constructor(function)
&& !NodeUtil.getFunctionParameters(function).hasChildren();
}
private void checkParams(NodeTraversal t, Node function, JSDocInfo jsDoc) {
if (jsDoc != null && jsDoc.getType() != null) {
// Sometimes functions are declared with @type {function(Foo, Bar)} instead of
// @param {Foo} foo
// @param {Bar} bar
// which is fine.
return;
}
List paramsFromJsDoc =
jsDoc == null
? ImmutableList.of()
: ImmutableList.copyOf(jsDoc.getParameterNames());
if (paramsFromJsDoc.isEmpty()) {
checkInlineParams(t, function, jsDoc);
} else {
Node paramList = NodeUtil.getFunctionParameters(function);
if (!paramList.hasXChildren(paramsFromJsDoc.size())) {
compiler.report(
JSError.make(
paramList,
WRONG_NUMBER_OF_PARAMS,
jsDoc.isOverride()
?
""
: ""));
return;
}
Node param = paramList.getFirstChild();
for (String s : paramsFromJsDoc) {
if (param.getJSDocInfo() != null) {
t.report(param, MIXED_PARAM_JSDOC_STYLES);
}
String name = s;
JSTypeExpression paramType = jsDoc.getParameterType(name);
if (checkParam(t, param, name, paramType)) {
return;
}
param = param.getNext();
}
}
}
/** Checks that the inline type annotations are correct. */
private void checkInlineParams(NodeTraversal t, Node function, JSDocInfo fnJSDoc) {
Node paramList = NodeUtil.getFunctionParameters(function);
for (Node param = paramList.getFirstChild(); param != null; param = param.getNext()) {
JSDocInfo jsDoc =
param.isDefaultValue() ? param.getFirstChild().getJSDocInfo() : param.getJSDocInfo();
if (jsDoc == null) {
compiler.report(
JSError.make(
param,
MISSING_PARAMETER_JSDOC,
fnJSDoc != null && fnJSDoc.isOverride()
?
""
: ""));
return;
} else {
JSTypeExpression paramType = jsDoc.getType();
checkNotNull(paramType, "Inline JSDoc info should always have a type");
checkParam(t, param, null, paramType);
}
}
}
/**
* Checks that the given parameter node has the given name, and that the given type is compatible.
*
* @param param If this is a non-NAME node, such as a destructuring pattern, skip the name check.
* @param name If null, skip the name check
* @return Whether a warning was reported
*/
private boolean checkParam(
NodeTraversal t, Node param, @Nullable String name, @Nullable JSTypeExpression paramType) {
boolean nameOptional;
Node nodeToCheck = param;
if (param.isDefaultValue()) {
nodeToCheck = param.getFirstChild();
nameOptional = true;
} else if (param.isName()) {
nameOptional = param.getString().startsWith("opt_");
} else {
checkState(param.isDestructuringPattern() || param.isRest(), param);
nameOptional = false;
}
if (name == null || !nodeToCheck.isName()) {
// Skip the name check, but use "" for other errors that might be reported.
name = "";
} else if (!nodeToCheck.matchesQualifiedName(name)) {
t.report(nodeToCheck, INCORRECT_PARAM_NAME);
return true;
}
if (!nameOptional) {
return false;
}
boolean jsDocOptional = paramType != null && paramType.isOptionalArg();
if (jsDocOptional) {
return false;
}
Node errorSource = paramType != null ? paramType.getRoot() : nodeToCheck;
t.report(errorSource, OPTIONAL_PARAM_NOT_MARKED_OPTIONAL, name);
return true;
}
private static boolean isDefaultAssignedParamWithInlineJsDoc(Node param) {
if (param.isDefaultValue()) {
if (param.hasChildren() && param.getFirstChild().isName()) {
return param.getFirstChild().getJSDocInfo() != null;
}
}
return false;
}
private boolean hasAnyInlineJsDoc(Node function) {
if (function.getFirstChild().getJSDocInfo() != null) {
// Inline return annotation.
return true;
}
for (Node param = NodeUtil.getFunctionParameters(function).getFirstChild();
param != null;
param = param.getNext()) {
if (param.getJSDocInfo() != null || isDefaultAssignedParamWithInlineJsDoc(param)) {
return true;
}
}
return false;
}
private void checkReturn(Node function, JSDocInfo jsDoc) {
if (jsDoc != null && (jsDoc.hasType() || jsDoc.isConstructor() || jsDoc.hasReturnType())) {
return;
}
if (NodeUtil.isEs6Constructor(function)) {
// ES6 class constructors should never have "@return".
return;
}
if (function.getFirstChild().getJSDocInfo() != null) {
return;
}
FindNonTrivialReturn finder = new FindNonTrivialReturn();
NodeTraversal.traverse(compiler, function.getLastChild(), finder);
if (finder.found) {
compiler.report(
JSError.make(
function,
MISSING_RETURN_JSDOC,
jsDoc != null && jsDoc.isOverride()
?
""
: ""));
}
}
private static class FindNonTrivialReturn extends AbstractPreOrderCallback {
private boolean found;
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
if (found) {
return false;
}
// Shallow traversal, since we don't need to inspect within functions or expressions.
if (parent == null
|| NodeUtil.isControlStructure(parent)
|| NodeUtil.isStatementBlock(parent)) {
if (n.isReturn() && n.hasChildren()) {
found = true;
return false;
}
return true;
}
return false;
}
}
private static class ExternsCallback implements NodeTraversal.Callback {
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
return parent == null || n.isScript();
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isScript()) {
JSDocInfo info = n.getJSDocInfo();
if (info == null || !info.isExterns()) {
t.report(n, EXTERNS_FILES_SHOULD_BE_ANNOTATED);
}
}
}
}
}