com.google.javascript.jscomp.lint.CheckNullabilityModifiers Maven / Gradle / Ivy
Show all versions of closure-compiler-unshaded Show documentation
/*
* Copyright 2018 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 com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.CompilerPass;
import com.google.javascript.jscomp.DiagnosticType;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
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 com.google.javascript.rhino.Token;
import java.util.HashSet;
import org.jspecify.nullness.Nullable;
/**
* Checks for missing or redundant nullability modifiers.
*
* Primitive and literal types should not be preceded by a `!` modifier. Reference types must be
* preceded by a `?` or `!` modifier.
*/
public class CheckNullabilityModifiers extends AbstractPostOrderCallback implements CompilerPass {
/** The diagnostic for a missing nullability modifier. */
public static final DiagnosticType MISSING_NULLABILITY_MODIFIER_JSDOC =
DiagnosticType.disabled(
"JSC_MISSING_NULLABILITY_MODIFIER_JSDOC",
"{0} is a reference type with no nullability modifier, "
+ "which is disallowed by the style guide.\n"
+ "Please add a '!' to make it explicitly non-nullable, "
+ "or a '?' to make it explicitly nullable.");
/** The diagnostic for a missing nullability modifier, where the value is clearly nullable. */
public static final DiagnosticType NULL_MISSING_NULLABILITY_MODIFIER_JSDOC =
DiagnosticType.disabled(
"JSC_NULL_MISSING_NULLABILITY_MODIFIER_JSDOC",
"{0} is a reference type with no nullability modifier that is explicitly set to null.\n"
+ "Add a '?' to make it explicitly nullable.");
/** The diagnostic for a redundant nullability modifier. */
public static final DiagnosticType REDUNDANT_NULLABILITY_MODIFIER_JSDOC =
DiagnosticType.disabled(
"JSC_REDUNDANT_NULLABILITY_MODIFIER_JSDOC",
"{0} is a non-reference type which is already non-nullable.\n"
+ "Please remove the redundant '!', which is disallowed by the style guide.");
/** The set of primitive type names. Note that `void` is a synonym for `undefined`. */
private static final ImmutableSet PRIMITIVE_TYPE_NAMES =
ImmutableSet.of(
"boolean", "number", "bigint", "string", "symbol", "undefined", "void", "null");
private final AbstractCompiler compiler;
// Store the candidate warnings and template types found while traversing a single script node.
private final HashSet redundantCandidates = new HashSet<>();
private final HashSet missingCandidates = new HashSet<>();
private final HashSet nullMissingCandidates = new HashSet<>();
private final HashSet templateTypeNames = new HashSet<>();
public CheckNullabilityModifiers(AbstractCompiler compiler) {
this.compiler = compiler;
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverseRoots(compiler, this, externs, root);
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
JSDocInfo info = n.getJSDocInfo();
if (info != null) {
templateTypeNames.addAll(info.getTemplateTypeNames());
templateTypeNames.addAll(info.getTypeTransformations().keySet());
if (info.hasType()) {
handleHasType(info.getType(), n);
}
for (String param : info.getParameterNames()) {
if (info.hasParameterType(param)) {
visitTypeExpression(info.getParameterType(param), false);
}
}
if (info.hasReturnType()) {
visitTypeExpression(info.getReturnType(), false);
}
if (info.hasEnumParameterType()) {
// JSDocInfoParser wraps the @enum type in an artificial '!' if it's not a primitive type.
// Thus a missing modifier warning can never trigger, but a redundant modifier warning can.
visitTypeExpression(info.getEnumParameterType(), false);
}
if (info.hasTypedefType()) {
visitTypeExpression(info.getTypedefType(), false);
}
if (info.hasThisType()) {
// JSDocInfoParser wraps the @this type in an artificial '!'. Thus a missing modifier
// warning can never trigger, but a top-level '!' should be ignored if it precedes a
// primitive or literal type; otherwise it will trigger a spurious redundant modifier
// warning.
visitTypeExpression(info.getThisType(), true);
}
// JSDocInfoParser enforces the @extends and @implements types to be unqualified in the source
// code, so we don't need to check them.
}
if (n.isScript()) {
// If exiting a script node, report applicable warnings and clear the maps.
report(t);
redundantCandidates.clear();
missingCandidates.clear();
nullMissingCandidates.clear();
templateTypeNames.clear();
return;
}
}
private void handleHasType(JSTypeExpression expr, Node n) {
// Check if the type is explicitly set to null, if so use NULL_MISSING_NULLABILITY diagnostic.
if (NodeUtil.isNameDeclOrSimpleAssignLhs(n.getFirstChild(), n)) {
Node rValue = NodeUtil.getRValueOfLValue(n.getFirstChild());
if (rValue != null && rValue.isNull()) {
visitTypeExpression(expr, false, rValue);
return;
}
}
visitTypeExpression(expr, false);
}
private void report(NodeTraversal t) {
for (Node n : missingCandidates) {
if (shouldReport(n)) {
t.report(n, MISSING_NULLABILITY_MODIFIER_JSDOC, getReportedTypeName(n));
}
}
for (Node n : nullMissingCandidates) {
if (shouldReport(n)) {
t.report(n, NULL_MISSING_NULLABILITY_MODIFIER_JSDOC, getReportedTypeName(n));
}
}
for (Node n : redundantCandidates) {
if (shouldReport(n)) {
// Report on parent so that modifier is also highlighted.
t.report(n.getParent(), REDUNDANT_NULLABILITY_MODIFIER_JSDOC, getReportedTypeName(n));
}
}
}
private boolean shouldReport(Node n) {
// Ignore type names that appear in @template clauses in the same source file. In rare cases,
// this may cause a false negative (if the name is used in a non-template capacity in the same
// file) or a false positive (if the name is used in a template capacity in a separate file),
// but it makes this check possible in the absence of type information. If the style guide ever
// mandates template types (and nothing else) to be all-caps, we can use that assumption to make
// this check more precise.
return !n.isStringLit() || !templateTypeNames.contains(n.getString());
}
private void visitTypeExpression(JSTypeExpression expr, boolean hasArtificialTopLevelBang) {
visitTypeExpression(expr, hasArtificialTopLevelBang, /* rValue= */ null);
}
private void visitTypeExpression(
JSTypeExpression expr, boolean hasArtificialTopLevelBang, @Nullable Node rValue) {
Node root = expr.getRoot();
NodeUtil.visitPreOrder(
root,
(Node node) -> {
Node parent = node.getParent();
boolean isPrimitiveOrLiteral =
isPrimitiveType(node) || isFunctionLiteral(node) || isRecordLiteral(node);
boolean isReference = isReferenceType(node);
// Whether the node is preceded by a '!' or '?'.
boolean hasBang = parent != null && parent.getToken() == Token.BANG;
boolean hasQmark = parent != null && parent.getToken() == Token.QMARK;
// Whether the node is preceded by a '!' that wasn't artificially added.
boolean hasNonArtificialBang = hasBang && !(hasArtificialTopLevelBang && parent == root);
// Whether the node is the type in function(new:T) or function(this:T).
boolean isNewOrThis = parent != null && (parent.isNew() || parent.isThis());
// Whether the node is the type name in a typeof expression.
boolean isTypeOfType = parent != null && parent.isTypeOf();
// Whether the node is definitely a possible type for the rValue e.g. 'Type' in @type
// {Type} or @type {Type|number} but not in @type {!Array}
boolean isAppliedToRValue =
node == root || (parent != null && parent.getToken() == Token.PIPE);
if (isReference && !hasBang && !hasQmark && !isNewOrThis && !isTypeOfType) {
if (isAppliedToRValue && rValue != null && rValue.isNull()) {
nullMissingCandidates.add(node);
} else {
missingCandidates.add(node);
}
} else if (isPrimitiveOrLiteral && hasNonArtificialBang) {
redundantCandidates.add(node);
}
});
}
private static boolean isPrimitiveType(Node node) {
return node.isStringLit() && PRIMITIVE_TYPE_NAMES.contains(node.getString());
}
private static boolean isReferenceType(Node node) {
return node.isStringLit() && !PRIMITIVE_TYPE_NAMES.contains(node.getString());
}
private static boolean isFunctionLiteral(Node node) {
return node.isFunction();
}
private static boolean isRecordLiteral(Node node) {
return node.getToken() == Token.LC;
}
private static String getReportedTypeName(Node node) {
if (isFunctionLiteral(node)) {
return "Function";
}
if (isRecordLiteral(node)) {
return "Record literal";
}
Preconditions.checkState(isPrimitiveType(node) || isReferenceType(node));
return node.getString();
}
}