All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.lint.CheckNullabilityModifiers Maven / Gradle / Ivy

There is a newer version: 9.0.8
Show newest version
/*
 * 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;

/**
 * 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, 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(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy