/** * Check for duplicate case labels in a switch statement Eg: switch (foo) { case 1: case 1: } * * <p>This is normally an indication of a programmer error. * * <p>Inspired by ESLint * (https://github.com/eslint/eslint/blob/master/lib/rules/no-duplicate-case.js) * * <p>TODO(moz): Move this into {@link CheckSuspiciousCode}. */ public final class CheckDuplicateCase extends AbstractPostOrderCallback implements HotSwapCompilerPass { public static final DiagnosticType DUPLICATE_CASE = DiagnosticType.warning("JSC_DUPLICATE_CASE", "Duplicate case in a switch statement."); private final AbstractCompiler compiler; public CheckDuplicateCase(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { 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) { if (n.isSwitch()) { Set<String> cases = new HashSet<>(); for (Node curr = n.getSecondChild(); curr != null; curr = curr.getNext()) { String source = compiler.toSource(curr.getFirstChild()); if (!cases.add(source)) { t.report(curr, DUPLICATE_CASE); } } } } }
/** * @author [email protected] (Ben Lickly) * @author [email protected] (Dimitris Vardoulakis) */ public final class JSTypeCreatorFromJSDoc { public static final DiagnosticType INVALID_GENERICS_INSTANTIATION = DiagnosticType.warning( "JSC_NTI_INVALID_GENERICS_INSTANTIATION", "Invalid generics instantiation for {0}.\n" + "Expected {1} type argument(s), but found {2}"); public static final DiagnosticType EXTENDS_NON_OBJECT = DiagnosticType.warning("JSC_NTI_EXTENDS_NON_OBJECT", "{0} extends non-object type {1}.\n"); public static final DiagnosticType EXTENDS_NOT_ON_CTOR_OR_INTERF = DiagnosticType.warning( "JSC_NTI_EXTENDS_NOT_ON_CTOR_OR_INTERF", "@extends used without @constructor or @interface for {0}.\n"); public static final DiagnosticType INHERITANCE_CYCLE = DiagnosticType.warning( "JSC_NTI_INHERITANCE_CYCLE", "Cycle detected in inheritance chain of type {0}"); public static final DiagnosticType DICT_IMPLEMENTS_INTERF = DiagnosticType.warning( "JSC_NTI_DICT_IMPLEMENTS_INTERF", "Class {0} is a dict. Dicts can't implement interfaces"); public static final DiagnosticType IMPLEMENTS_WITHOUT_CONSTRUCTOR = DiagnosticType.warning( "JSC_NTI_IMPLEMENTS_WITHOUT_CONSTRUCTOR", "@implements used without @constructor or @interface for {0}"); // Not part of ALL_DIAGNOSTICS because it should not be enabled with // --jscomp_error=newCheckTypes. It should only be enabled explicitly. public static final DiagnosticType CONFLICTING_SHAPE_TYPE = DiagnosticType.disabled( "JSC_NTI_CONFLICTING_SHAPE_TYPE", "{1} cannot extend this type; {0}s can only extend {0}s"); public static final DiagnosticType CONFLICTING_EXTENDED_TYPE = DiagnosticType.warning( "JSC_NTI_CONFLICTING_EXTENDED_TYPE", "{1} cannot extend this type; {0}s can only extend {0}s"); public static final DiagnosticType CONFLICTING_IMPLEMENTED_TYPE = DiagnosticType.warning( "JSC_NTI_CONFLICTING_IMPLEMENTED_TYPE", "{0} cannot implement this type; " + "an interface can only extend, but not implement interfaces"); public static final DiagnosticType UNION_IS_UNINHABITABLE = DiagnosticType.warning( "JSC_NTI_UNION_IS_UNINHABITABLE", "Union of {0} with {1} would create an impossible type"); public static final DiagnosticType NEW_EXPECTS_OBJECT_OR_TYPEVAR = DiagnosticType.warning( "JSC_NTI_NEW_EXPECTS_OBJECT_OR_TYPEVAR", "The \"new:\" annotation only accepts object types and type variables; " + "found {0}"); public static final DiagnosticType BAD_ARRAY_TYPE_SYNTAX = DiagnosticType.warning( "JSC_NTI_BAD_ARRAY_TYPE_SYNTAX", "The [] type syntax is not supported. Please use Array.<T> instead"); public static final DiagnosticType CANNOT_MAKE_TYPEVAR_NON_NULL = DiagnosticType.warning( "JSC_NTI_CANNOT_MAKE_TYPEVAR_NON_NULL", "Cannot use ! to restrict type variable type.\n" + "Prefer to make type argument non-nullable and add " + "null explicitly where needed (e.g. through ?T or T|null)"); public static final DiagnosticType CIRCULAR_TYPEDEF_ENUM = DiagnosticType.warning( "JSC_NTI_CIRCULAR_TYPEDEF_ENUM", "Circular typedefs/enums are not allowed"); public static final DiagnosticType ENUM_WITH_TYPEVARS = DiagnosticType.warning( "JSC_NTI_ENUM_WITH_TYPEVARS", "An enum type cannot include type variables"); public static final DiagnosticType ENUM_IS_TOP = DiagnosticType.warning( "JSC_NTI_ENUM_IS_TOP", "An enum type cannot be *. " + "Use ? if you do not want the elements checked"); // TODO(dimvar): This may prove to be too strict, may revisit. public static final DiagnosticType ENUM_IS_UNION = DiagnosticType.warning("JSC_NTI_ENUM_IS_UNION", "An enum type cannot be a union type"); public static final DiagnosticType WRONG_PARAMETER_ORDER = DiagnosticType.warning( "JSC_NTI_WRONG_PARAMETER_ORDER", "Wrong parameter order: required parameters are first, " + "then optional, then varargs"); public static final DiagnosticType IMPLEMENTS_NON_INTERFACE = DiagnosticType.warning( "JSC_NTI_IMPLEMENTS_NON_INTERFACE", "Cannot implement non-interface {0}"); public static final DiagnosticType EXTENDS_NON_INTERFACE = DiagnosticType.warning("JSC_NTI_EXTENDS_NON_INTERFACE", "Cannot extend non-interface {0}"); public static final DiagnosticType FUNCTION_WITH_NONFUNC_JSDOC = DiagnosticType.warning( "JSC_NTI_FUNCTION_WITH_NONFUNC_JSDOC", "The function is annotated with a non-function jsdoc. " + "Ignoring jsdoc"); public static final DiagnosticType TEMPLATED_GETTER_SETTER = DiagnosticType.warning( "JSC_NTI_TEMPLATED_GETTER_SETTER", "@template can't be used with getters/setters"); public static final DiagnosticType TWO_JSDOCS = DiagnosticType.warning("JSC_NTI_TWO_JSDOCS", "Found two JsDoc comments for {0}"); public static final DiagnosticGroup ALL_DIAGNOSTICS = new DiagnosticGroup( BAD_ARRAY_TYPE_SYNTAX, CANNOT_MAKE_TYPEVAR_NON_NULL, CIRCULAR_TYPEDEF_ENUM, CONFLICTING_EXTENDED_TYPE, CONFLICTING_IMPLEMENTED_TYPE, DICT_IMPLEMENTS_INTERF, ENUM_IS_TOP, ENUM_IS_UNION, ENUM_WITH_TYPEVARS, EXTENDS_NON_INTERFACE, EXTENDS_NON_OBJECT, EXTENDS_NOT_ON_CTOR_OR_INTERF, FUNCTION_WITH_NONFUNC_JSDOC, IMPLEMENTS_NON_INTERFACE, IMPLEMENTS_WITHOUT_CONSTRUCTOR, INHERITANCE_CYCLE, INVALID_GENERICS_INSTANTIATION, NEW_EXPECTS_OBJECT_OR_TYPEVAR, TEMPLATED_GETTER_SETTER, TWO_JSDOCS, UNION_IS_UNINHABITABLE, WRONG_PARAMETER_ORDER); private final CodingConvention convention; private final UniqueNameGenerator nameGen; // Used to communicate state between methods when resolving enum types private int howmanyTypeVars = 0; /** Exception for when unrecognized type names are encountered */ public static class UnknownTypeException extends Exception { UnknownTypeException(String cause) { super(cause); } } private Set<JSError> warnings = new LinkedHashSet<>(); // Unknown type names indexed by JSDoc AST node at which they were found. private Map<Node, String> unknownTypeNames = new LinkedHashMap<>(); public JSTypeCreatorFromJSDoc(CodingConvention convention, UniqueNameGenerator nameGen) { this.qmarkFunctionDeclared = new FunctionAndSlotType( null, FunctionTypeBuilder.qmarkFunctionBuilder().buildDeclaration()); this.convention = convention; this.nameGen = nameGen; } private FunctionAndSlotType qmarkFunctionDeclared; private static final boolean NULLABLE_TYPES_BY_DEFAULT = true; public JSType maybeMakeNullable(JSType t) { if (NULLABLE_TYPES_BY_DEFAULT) { return JSType.join(JSType.NULL, t); } return t; } public JSType getDeclaredTypeOfNode( JSDocInfo jsdoc, RawNominalType ownerType, DeclaredTypeRegistry registry) { return getDeclaredTypeOfNode( jsdoc, registry, ownerType == null ? ImmutableList.<String>of() : ownerType.getTypeParameters()); } private JSType getDeclaredTypeOfNode( JSDocInfo jsdoc, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters) { if (jsdoc == null) { return null; } return getTypeFromJSTypeExpression(jsdoc.getType(), registry, typeParameters); } public Set<JSError> getWarnings() { return warnings; } public Map<Node, String> getUnknownTypesMap() { return unknownTypeNames; } private JSType getTypeFromJSTypeExpression( JSTypeExpression expr, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters) { if (expr == null) { return null; } return getTypeFromComment(expr.getRoot(), registry, typeParameters); } // Very similar to JSTypeRegistry#createFromTypeNodesInternal // n is a jsdoc node, not an AST node; the same class (Node) is used for both private JSType getTypeFromComment( Node n, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters) { try { return getTypeFromCommentHelper(n, registry, typeParameters); } catch (UnknownTypeException e) { return JSType.UNKNOWN; } } private JSType getMaybeTypeFromComment( Node n, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters) { try { return getTypeFromCommentHelper(n, registry, typeParameters); } catch (UnknownTypeException e) { return null; } } private JSType getTypeFromCommentHelper( Node n, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters) throws UnknownTypeException { Preconditions.checkNotNull(n); if (typeParameters == null) { typeParameters = ImmutableList.of(); } switch (n.getType()) { case Token.LC: return getRecordTypeHelper(n, registry, typeParameters); case Token.EMPTY: // for function types that don't declare a return type return JSType.UNKNOWN; case Token.VOID: // TODO(dimvar): void can be represented in 2 ways: Token.VOID and a // Token.STRING whose getString() is "void". // Change jsdoc parsing to only have one representation. return JSType.UNDEFINED; case Token.LB: warnings.add(JSError.make(n, BAD_ARRAY_TYPE_SYNTAX)); return JSType.UNKNOWN; case Token.STRING: return getNamedTypeHelper(n, registry, typeParameters); case Token.PIPE: { // The way JSType.join works, Subtype|Supertype is equal to Supertype, // so when programmers write un-normalized unions, we normalize them // silently. We may also want to warn. JSType union = JSType.BOTTOM; for (Node child = n.getFirstChild(); child != null; child = child.getNext()) { // TODO(dimvar): When the union has many things, we join and throw // away types, except the result of the last join. Very inefficient. // Consider optimizing. JSType nextType = getTypeFromCommentHelper(child, registry, typeParameters); if (nextType.isUnknown()) { return JSType.UNKNOWN; } JSType nextUnion = JSType.join(union, nextType); if (nextUnion.isBottom()) { warnings.add( JSError.make(n, UNION_IS_UNINHABITABLE, nextType.toString(), union.toString())); return JSType.UNKNOWN; } union = nextUnion; } return union; } case Token.BANG: { JSType nullableType = getTypeFromCommentHelper(n.getFirstChild(), registry, typeParameters); if (nullableType.isTypeVariable()) { warnings.add(JSError.make(n, CANNOT_MAKE_TYPEVAR_NON_NULL)); } return nullableType.removeType(JSType.NULL); } case Token.QMARK: { Node child = n.getFirstChild(); if (child == null) { return JSType.UNKNOWN; } else { return JSType.join( JSType.NULL, getTypeFromCommentHelper(child, registry, typeParameters)); } } case Token.STAR: return JSType.TOP; case Token.FUNCTION: return getFunTypeHelper(n, registry, typeParameters); default: throw new IllegalArgumentException( "Unsupported type exp: " + Token.name(n.getType()) + " " + n.toStringTree()); } } // Looks at the type AST without evaluating it private boolean isUnionWithUndefined(Node n) { if (n == null || n.getType() != Token.PIPE) { return false; } for (Node child : n.children()) { if (child.getType() == Token.VOID || child.getType() == Token.STRING && (child.getString().equals("void") || child.getString().equals("undefined"))) { return true; } } return false; } private JSType getRecordTypeHelper( Node n, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters) throws UnknownTypeException { Map<String, Property> props = new LinkedHashMap<>(); for (Node propNode = n.getFirstFirstChild(); propNode != null; propNode = propNode.getNext()) { boolean isPropDeclared = propNode.getType() == Token.COLON; Node propNameNode = isPropDeclared ? propNode.getFirstChild() : propNode; String propName = propNameNode.getString(); if (propName.startsWith("'") || propName.startsWith("\"")) { propName = propName.substring(1, propName.length() - 1); } JSType propType = !isPropDeclared ? JSType.UNKNOWN : getTypeFromCommentHelper(propNode.getLastChild(), registry, typeParameters); Property prop; if (propType.equals(JSType.UNDEFINED) || isUnionWithUndefined(propNode.getLastChild())) { prop = Property.makeOptional(null, propType, propType); } else { prop = Property.make(propType, propType); } props.put(propName, prop); } return JSType.fromObjectType(ObjectType.fromProperties(props)); } private JSType getNamedTypeHelper( Node n, DeclaredTypeRegistry registry, ImmutableList<String> outerTypeParameters) throws UnknownTypeException { String typeName = n.getString(); switch (typeName) { case "boolean": return JSType.BOOLEAN; case "null": return JSType.NULL; case "number": return JSType.NUMBER; case "string": return JSType.STRING; case "undefined": case "void": return JSType.UNDEFINED; case "Function": return maybeMakeNullable(registry.getCommonTypes().qmarkFunction()); case "Object": // We don't generally handle parameterized Object<...>, but we want to // at least not warn about inexistent properties on it, so we type it // as @dict. return maybeMakeNullable(n.hasChildren() ? JSType.TOP_DICT : JSType.TOP_OBJECT); default: return lookupTypeByName(typeName, n, registry, outerTypeParameters); } } private JSType lookupTypeByName( String name, Node n, DeclaredTypeRegistry registry, ImmutableList<String> outerTypeParameters) throws UnknownTypeException { String tvar = UniqueNameGenerator.findGeneratedName(name, outerTypeParameters); if (tvar != null) { return JSType.fromTypeVar(tvar); } Declaration decl = registry.getDeclaration(QualifiedName.fromQualifiedString(name), true); if (decl == null) { unknownTypeNames.put(n, name); throw new UnknownTypeException("Unhandled type: " + name); } // It's either a typedef, an enum, a type variable or a nominal type if (decl.getTypedef() != null) { return getTypedefType(decl.getTypedef(), registry); } if (decl.getEnum() != null) { return getEnumPropType(decl.getEnum(), registry); } if (decl.isTypeVar()) { howmanyTypeVars++; return decl.getTypeOfSimpleDecl(); } if (decl.getNominal() != null) { return getNominalTypeHelper(decl.getNominal(), n, registry, outerTypeParameters); } return JSType.UNKNOWN; } private JSType getTypedefType(Typedef td, DeclaredTypeRegistry registry) { resolveTypedef(td, registry); return td.getType(); } public void resolveTypedef(Typedef td, DeclaredTypeRegistry registry) { Preconditions.checkState( td != null, "getTypedef should only be " + "called when we know that the typedef is defined"); if (td.isResolved()) { return; } JSTypeExpression texp = td.getTypeExpr(); JSType tdType; if (texp == null) { warnings.add( JSError.make(td.getTypeExprForErrorReporting().getRoot(), CIRCULAR_TYPEDEF_ENUM)); tdType = JSType.UNKNOWN; } else { tdType = getTypeFromJSTypeExpression(texp, registry, null); } td.resolveTypedef(tdType); } private JSType getEnumPropType(EnumType e, DeclaredTypeRegistry registry) { resolveEnum(e, registry); return e.getPropType(); } public void resolveEnum(EnumType e, DeclaredTypeRegistry registry) { Preconditions.checkState( e != null, "getEnum should only be " + "called when we know that the enum is defined"); if (e.isResolved()) { return; } JSTypeExpression texp = e.getTypeExpr(); JSType enumeratedType; if (texp == null) { warnings.add(JSError.make(e.getTypeExprForErrorReporting().getRoot(), CIRCULAR_TYPEDEF_ENUM)); enumeratedType = JSType.UNKNOWN; } else { int numTypeVars = howmanyTypeVars; enumeratedType = getTypeFromJSTypeExpression(texp, registry, null); if (howmanyTypeVars > numTypeVars) { warnings.add(JSError.make(texp.getRoot(), ENUM_WITH_TYPEVARS)); enumeratedType = JSType.UNKNOWN; howmanyTypeVars = numTypeVars; } else if (enumeratedType.isTop()) { warnings.add(JSError.make(texp.getRoot(), ENUM_IS_TOP)); enumeratedType = JSType.UNKNOWN; } else if (enumeratedType.isUnion()) { warnings.add(JSError.make(texp.getRoot(), ENUM_IS_UNION)); enumeratedType = JSType.UNKNOWN; } } e.resolveEnum(enumeratedType); } private JSType getNominalTypeHelper( RawNominalType rawType, Node n, DeclaredTypeRegistry registry, ImmutableList<String> outerTypeParameters) throws UnknownTypeException { NominalType uninstantiated = rawType.getAsNominalType(); if (!rawType.isGeneric() && !n.hasChildren()) { return rawType.getInstanceWithNullability(NULLABLE_TYPES_BY_DEFAULT); } ImmutableList.Builder<JSType> typeList = ImmutableList.builder(); if (n.hasChildren()) { // Compute instantiation of polymorphic class/interface. Preconditions.checkState(n.getFirstChild().isBlock(), n); for (Node child : n.getFirstChild().children()) { typeList.add(getTypeFromCommentHelper(child, registry, outerTypeParameters)); } } ImmutableList<JSType> typeArguments = typeList.build(); ImmutableList<String> typeParameters = rawType.getTypeParameters(); int typeArgsSize = typeArguments.size(); int typeParamsSize = typeParameters.size(); if (typeArgsSize != typeParamsSize) { // We used to also warn when (typeArgsSize < typeParamsSize), but it // happens so often that we stopped. Array, Object and goog.Promise are // common culprits, but many other types as well. if (typeArgsSize > typeParamsSize) { warnings.add( JSError.make( n, INVALID_GENERICS_INSTANTIATION, uninstantiated.getName(), String.valueOf(typeParamsSize), String.valueOf(typeArgsSize))); } return maybeMakeNullable( JSType.fromObjectType( ObjectType.fromNominalType( uninstantiated.instantiateGenerics( fixLengthOfTypeList(typeParameters.size(), typeArguments))))); } return maybeMakeNullable( JSType.fromObjectType( ObjectType.fromNominalType(uninstantiated.instantiateGenerics(typeArguments)))); } private static List<JSType> fixLengthOfTypeList(int desiredLength, List<JSType> typeList) { int length = typeList.size(); if (length == desiredLength) { return typeList; } ImmutableList.Builder<JSType> builder = ImmutableList.builder(); for (int i = 0; i < desiredLength; i++) { builder.add(i < length ? typeList.get(i) : JSType.UNKNOWN); } return builder.build(); } // Computes a type from a jsdoc that includes a function type, rather than // one that includes @param, @return, etc. private JSType getFunTypeHelper( Node jsdocNode, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters) throws UnknownTypeException { FunctionTypeBuilder builder = new FunctionTypeBuilder(); fillInFunTypeBuilder(jsdocNode, null, registry, typeParameters, builder); return registry.getCommonTypes().fromFunctionType(builder.buildFunction()); } private void fillInFunTypeBuilder( Node jsdocNode, RawNominalType ownerType, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters, FunctionTypeBuilder builder) throws UnknownTypeException { Node child = jsdocNode.getFirstChild(); if (child.getType() == Token.THIS) { if (ownerType == null) { builder.addReceiverType(getThisOrNewType(child.getFirstChild(), registry, typeParameters)); } child = child.getNext(); } else if (child.getType() == Token.NEW) { Node newTypeNode = child.getFirstChild(); JSType t = getThisOrNewType(newTypeNode, registry, typeParameters); if (!t.isSubtypeOf(JSType.TOP_OBJECT) && (!t.hasTypeVariable() || t.hasScalar())) { warnings.add(JSError.make(newTypeNode, NEW_EXPECTS_OBJECT_OR_TYPEVAR, t.toString())); } builder.addNominalType(t); child = child.getNext(); } if (child.getType() == Token.PARAM_LIST) { for (Node arg = child.getFirstChild(); arg != null; arg = arg.getNext()) { try { switch (arg.getType()) { case Token.EQUALS: builder.addOptFormal( getTypeFromCommentHelper(arg.getFirstChild(), registry, typeParameters)); break; case Token.ELLIPSIS: Node restNode = arg.getFirstChild(); builder.addRestFormals( restNode == null ? JSType.UNKNOWN : getTypeFromCommentHelper(restNode, registry, typeParameters)); break; default: builder.addReqFormal(getTypeFromCommentHelper(arg, registry, typeParameters)); break; } } catch (FunctionTypeBuilder.WrongParameterOrderException e) { warnings.add(JSError.make(jsdocNode, WRONG_PARAMETER_ORDER)); builder.addPlaceholderFormal(); } } child = child.getNext(); } builder.addRetType(getTypeFromCommentHelper(child, registry, typeParameters)); } private JSType getThisOrNewType( Node n, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters) { JSType t = getTypeFromComment(n, registry, typeParameters); return t.isSingletonObjWithNull() ? t.removeType(JSType.NULL) : t; } private ImmutableSet<NominalType> getImplementedInterfaces( JSDocInfo jsdoc, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters) { return getInterfacesHelper(jsdoc, registry, typeParameters, true); } private ImmutableSet<NominalType> getExtendedInterfaces( JSDocInfo jsdoc, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters) { return getInterfacesHelper(jsdoc, registry, typeParameters, false); } private ImmutableSet<NominalType> getInterfacesHelper( JSDocInfo jsdoc, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters, boolean implementedIntfs) { ImmutableSet.Builder<NominalType> builder = ImmutableSet.builder(); for (JSTypeExpression texp : (implementedIntfs ? jsdoc.getImplementedInterfaces() : jsdoc.getExtendedInterfaces())) { Node expRoot = texp.getRoot(); JSType interfaceType = getMaybeTypeFromComment(expRoot, registry, typeParameters); if (interfaceType != null) { NominalType nt = interfaceType.getNominalTypeIfSingletonObj(); if (nt != null && nt.isInterface()) { builder.add(nt); } else if (implementedIntfs) { warnings.add(JSError.make(expRoot, IMPLEMENTS_NON_INTERFACE, interfaceType.toString())); } else { warnings.add(JSError.make(expRoot, EXTENDS_NON_INTERFACE, interfaceType.toString())); } } } return builder.build(); } public static class FunctionAndSlotType { public JSType slotType; public DeclaredFunctionType functionType; public FunctionAndSlotType(JSType slotType, DeclaredFunctionType functionType) { this.slotType = slotType; this.functionType = functionType; } } /** * Consumes either a "classic" function jsdoc with @param, @return, etc, or a jsdoc with @type * {function ...} and finds the types of the formal parameters and the return value. It returns a * builder because the callers of this function must separately handle @constructor, @interface, * etc. * * <p>constructorType is non-null iff this function is a constructor or interface declaration. */ public FunctionAndSlotType getFunctionType( JSDocInfo jsdoc, String functionName, Node declNode, RawNominalType constructorType, RawNominalType ownerType, DeclaredTypeRegistry registry) { FunctionTypeBuilder builder = new FunctionTypeBuilder(); if (ownerType != null) { builder.addReceiverType(ownerType.getInstanceAsJSType()); } try { if (jsdoc != null && jsdoc.getType() != null) { JSType simpleType = getDeclaredTypeOfNode(jsdoc, ownerType, registry); if (simpleType.isUnknown() || simpleType.isTop()) { return qmarkFunctionDeclared; } FunctionType funType = simpleType.getFunType(); if (funType != null) { JSType slotType = simpleType.isFunctionType() ? null : simpleType; DeclaredFunctionType declType = funType.toDeclaredFunctionType(); if (ownerType != null && funType.getThisType() == null) { declType = declType.withReceiverType(ownerType.getInstanceAsJSType()); } return new FunctionAndSlotType(slotType, declType); } else { warnings.add(JSError.make(declNode, FUNCTION_WITH_NONFUNC_JSDOC)); jsdoc = null; } } DeclaredFunctionType declType = getFunTypeFromTypicalFunctionJsdoc( jsdoc, functionName, declNode, constructorType, ownerType, registry, builder); return new FunctionAndSlotType(null, declType); } catch (FunctionTypeBuilder.WrongParameterOrderException e) { warnings.add(JSError.make(declNode, WRONG_PARAMETER_ORDER)); return qmarkFunctionDeclared; } } private static class ParamIterator { /** The parameter names from the JSDocInfo. Only set if 'params' is null. */ Iterator<String> paramNames; /** The PARAM_LIST node containing the function parameters. Only set if 'paramNames' is null. */ Node params; int index = -1; ParamIterator(Node params, JSDocInfo jsdoc) { Preconditions.checkArgument(params != null || jsdoc != null); if (params != null) { this.params = params; this.paramNames = null; } else { this.params = null; this.paramNames = jsdoc.getParameterNames().iterator(); } } boolean hasNext() { if (paramNames != null) { return paramNames.hasNext(); } return index + 1 < params.getChildCount(); } String nextString() { if (paramNames != null) { return paramNames.next(); } index++; return params.getChildAtIndex(index).getString(); } Node getNode() { if (paramNames != null) { return null; } return params.getChildAtIndex(index); } } private DeclaredFunctionType getFunTypeFromTypicalFunctionJsdoc( JSDocInfo jsdoc, String functionName, Node funNode, RawNominalType constructorType, RawNominalType ownerType, DeclaredTypeRegistry registry, FunctionTypeBuilder builder) { ImmutableList.Builder<String> typeParamsBuilder = ImmutableList.builder(); ImmutableList<String> typeParameters = ImmutableList.of(); Node parent = funNode.getParent(); // TODO(dimvar): need more @template warnings // - warn for multiple @template annotations // - warn for @template annotation w/out usage boolean ignoreJsdoc = false; if (jsdoc != null) { if (constructorType != null) { // We have created new names for these type variables in GTI, don't // create new ones here. typeParamsBuilder.addAll(constructorType.getTypeParameters()); } else { for (String typeParam : jsdoc.getTemplateTypeNames()) { typeParamsBuilder.add(this.nameGen.getNextName(typeParam)); } } // We don't properly support the type transformation language; we treat // its type variables as ordinary type variables. for (String typeParam : jsdoc.getTypeTransformations().keySet()) { typeParamsBuilder.add(this.nameGen.getNextName(typeParam)); } typeParameters = typeParamsBuilder.build(); if (!typeParameters.isEmpty()) { if (parent.isSetterDef() || parent.isGetterDef()) { ignoreJsdoc = true; jsdoc = null; warnings.add(JSError.make(funNode, TEMPLATED_GETTER_SETTER)); } else { builder.addTypeParameters(typeParameters); } } } if (ownerType != null) { typeParamsBuilder.addAll(ownerType.getTypeParameters()); typeParameters = typeParamsBuilder.build(); } fillInFormalParameterTypes(jsdoc, funNode, typeParameters, registry, builder, ignoreJsdoc); fillInReturnType(jsdoc, funNode, parent, typeParameters, registry, builder, ignoreJsdoc); if (jsdoc == null) { return builder.buildDeclaration(); } // Look at other annotations, eg, @constructor NominalType parentClass = getMaybeParentClass(jsdoc, functionName, funNode, typeParameters, registry); ImmutableSet<NominalType> implementedIntfs = getImplementedInterfaces(jsdoc, registry, typeParameters); if (constructorType == null && jsdoc.isConstructorOrInterface()) { // Anonymous type, don't register it. return builder.buildDeclaration(); } else if (jsdoc.isConstructor()) { handleConstructorAnnotation( functionName, funNode, constructorType, parentClass, implementedIntfs, registry, builder); } else if (jsdoc.isInterface()) { handleInterfaceAnnotation( jsdoc, functionName, funNode, constructorType, implementedIntfs, typeParameters, registry, builder); } else if (!implementedIntfs.isEmpty()) { warnings.add(JSError.make(funNode, IMPLEMENTS_WITHOUT_CONSTRUCTOR, functionName)); } if (jsdoc.hasThisType()) { Node thisRoot = jsdoc.getThisType().getRoot(); Preconditions.checkState(thisRoot.getType() == Token.BANG); builder.addReceiverType(getThisOrNewType(thisRoot.getFirstChild(), registry, typeParameters)); } return builder.buildDeclaration(); } private void fillInFormalParameterTypes( JSDocInfo jsdoc, Node funNode, ImmutableList<String> typeParameters, DeclaredTypeRegistry registry, FunctionTypeBuilder builder, boolean ignoreJsdoc /* for when the jsdoc is malformed */) { boolean ignoreFunNode = !funNode.isFunction(); Node params = ignoreFunNode ? null : funNode.getSecondChild(); ParamIterator iterator = new ParamIterator(params, jsdoc); while (iterator.hasNext()) { String pname = iterator.nextString(); Node param = iterator.getNode(); ParameterKind p = ParameterKind.REQUIRED; if (param != null && convention.isOptionalParameter(param)) { p = ParameterKind.OPTIONAL; } else if (param != null && convention.isVarArgsParameter(param)) { p = ParameterKind.REST; } ParameterType inlineParamType = (ignoreJsdoc || ignoreFunNode || param.getJSDocInfo() == null) ? null : parseParameter(param.getJSDocInfo().getType(), p, registry, typeParameters); ParameterType fnParamType = inlineParamType; JSTypeExpression jsdocExp = jsdoc == null ? null : jsdoc.getParameterType(pname); if (jsdocExp != null) { if (inlineParamType == null) { fnParamType = parseParameter(jsdocExp, p, registry, typeParameters); } else { warnings.add(JSError.make(param, TWO_JSDOCS, "formal parameter " + pname)); } } JSType t = null; if (fnParamType != null) { p = fnParamType.kind; t = fnParamType.type; } switch (p) { case REQUIRED: builder.addReqFormal(t); break; case OPTIONAL: builder.addOptFormal(t); break; case REST: builder.addRestFormals(t != null ? t : JSType.UNKNOWN); break; } } } private void fillInReturnType( JSDocInfo jsdoc, Node funNode, Node parent, ImmutableList<String> typeParameters, DeclaredTypeRegistry registry, FunctionTypeBuilder builder, boolean ignoreJsdoc /* for when the jsdoc is malformed */) { JSDocInfo inlineRetJsdoc = ignoreJsdoc ? null : funNode.getFirstChild().getJSDocInfo(); JSTypeExpression retTypeExp = jsdoc == null ? null : jsdoc.getReturnType(); if (parent.isSetterDef() && retTypeExp == null) { // inline returns for getters/setters are not parsed builder.addRetType(JSType.UNDEFINED); } else if (inlineRetJsdoc != null) { builder.addRetType(getDeclaredTypeOfNode(inlineRetJsdoc, registry, typeParameters)); if (retTypeExp != null) { warnings.add(JSError.make(funNode, TWO_JSDOCS, "the return type")); } } else { builder.addRetType(getTypeFromJSTypeExpression(retTypeExp, registry, typeParameters)); } } private NominalType getMaybeParentClass( JSDocInfo jsdoc, String functionName, Node funNode, ImmutableList<String> typeParameters, DeclaredTypeRegistry registry) { if (!jsdoc.hasBaseType()) { return null; } if (!jsdoc.isConstructor()) { warnings.add(JSError.make(funNode, EXTENDS_NOT_ON_CTOR_OR_INTERF, functionName)); return null; } Node docNode = jsdoc.getBaseType().getRoot(); JSType extendedType = getMaybeTypeFromComment(docNode, registry, typeParameters); if (extendedType == null) { return null; } NominalType parentClass = extendedType.getNominalTypeIfSingletonObj(); if (parentClass != null && parentClass.isClass()) { return parentClass; } if (parentClass == null) { warnings.add( JSError.make(funNode, EXTENDS_NON_OBJECT, functionName, extendedType.toString())); } else { Preconditions.checkState(parentClass.isInterface()); warnings.add(JSError.make(funNode, CONFLICTING_EXTENDED_TYPE, "constructor", functionName)); } return null; } private void handleConstructorAnnotation( String functionName, Node funNode, RawNominalType constructorType, NominalType parentClass, ImmutableSet<NominalType> implementedIntfs, DeclaredTypeRegistry registry, FunctionTypeBuilder builder) { String className = constructorType.toString(); NominalType builtinObject = registry.getCommonTypes().getObjectType(); if (parentClass == null && !functionName.equals("Object")) { parentClass = builtinObject; } if (parentClass != null) { if (!constructorType.addSuperClass(parentClass)) { warnings.add(JSError.make(funNode, INHERITANCE_CYCLE, className)); } else if (parentClass != builtinObject) { if (constructorType.isStruct() && !parentClass.isStruct()) { warnings.add(JSError.make(funNode, CONFLICTING_SHAPE_TYPE, "struct", className)); } else if (constructorType.isDict() && !parentClass.isDict()) { warnings.add(JSError.make(funNode, CONFLICTING_SHAPE_TYPE, "dict", className)); } } } if (constructorType.isDict() && !implementedIntfs.isEmpty()) { warnings.add(JSError.make(funNode, DICT_IMPLEMENTS_INTERF, className)); } boolean noCycles = constructorType.addInterfaces(implementedIntfs); Preconditions.checkState(noCycles); builder.addNominalType(constructorType.getInstanceAsJSType()); } private void handleInterfaceAnnotation( JSDocInfo jsdoc, String functionName, Node funNode, RawNominalType constructorType, ImmutableSet<NominalType> implementedIntfs, ImmutableList<String> typeParameters, DeclaredTypeRegistry registry, FunctionTypeBuilder builder) { if (!implementedIntfs.isEmpty()) { warnings.add(JSError.make(funNode, CONFLICTING_IMPLEMENTED_TYPE, functionName)); } ImmutableSet<NominalType> extendedInterfaces = getExtendedInterfaces(jsdoc, registry, typeParameters); boolean noCycles = constructorType.addInterfaces( extendedInterfaces.isEmpty() ? ImmutableSet.of(registry.getCommonTypes().getObjectType()) : extendedInterfaces); if (!noCycles) { warnings.add(JSError.make(funNode, INHERITANCE_CYCLE, constructorType.toString())); } builder.addNominalType(constructorType.getInstanceAsJSType()); } // /** @param {...?} var_args */ function f(var_args) { ... } // var_args shouldn't be used in the body of f public static boolean isRestArg(JSDocInfo funJsdoc, String formalParamName) { if (funJsdoc == null) { return false; } JSTypeExpression texp = funJsdoc.getParameterType(formalParamName); Node jsdocNode = texp == null ? null : texp.getRoot(); return jsdocNode != null && jsdocNode.getType() == Token.ELLIPSIS; } private ParameterType parseParameter( JSTypeExpression jsdoc, ParameterKind p, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters) { if (jsdoc == null) { return null; } return parseParameter(jsdoc.getRoot(), p, registry, typeParameters); } private ParameterType parseParameter( Node jsdoc, ParameterKind p, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters) { if (jsdoc == null) { return null; } switch (jsdoc.getType()) { case Token.EQUALS: p = ParameterKind.OPTIONAL; jsdoc = jsdoc.getFirstChild(); break; case Token.ELLIPSIS: p = ParameterKind.REST; jsdoc = jsdoc.getFirstChild(); break; } JSType t = getMaybeTypeFromComment(jsdoc, registry, typeParameters); return new ParameterType(t, p); } private static class ParameterType { private JSType type; private ParameterKind kind; ParameterType(JSType type, ParameterKind kind) { this.type = type; this.kind = kind; } } private static enum ParameterKind { REQUIRED, OPTIONAL, REST, } }
/** * Checks when a function is annotated as returning {SomeType} (nullable) but actually always * returns {!SomeType}, i.e. never returns null. */ public final class CheckNullableReturn implements HotSwapCompilerPass, NodeTraversal.Callback { final AbstractCompiler compiler; public static final DiagnosticType NULLABLE_RETURN = DiagnosticType.warning( "JSC_NULLABLE_RETURN", "This function''s return type is nullable, but it always returns a " + "non-null value. Consider making the return type non-nullable."); public static final DiagnosticType NULLABLE_RETURN_WITH_NAME = DiagnosticType.warning( "JSC_NULLABLE_RETURN_WITH_NAME", "The return type of the function \"{0}\" is nullable, but it always " + "returns a non-null value. Consider making the return type " + "non-nullable."); private static final Predicate<Node> NULLABLE_RETURN_PREDICATE = new Predicate<Node>() { @Override public boolean apply(Node input) { // Check for null because the control flow graph's implicit return node is // represented by null, so this value might be input. if (input == null || !input.isReturn()) { return false; } Node returnValue = input.getFirstChild(); return returnValue != null && isNullable(returnValue); } }; public CheckNullableReturn(AbstractCompiler compiler) { this.compiler = compiler; } public static boolean hasReturnDeclaredNullable(Node n) { return n.isBlock() && n.hasChildren() && isReturnTypeNullable(n.getParent()) && !hasSingleThrow(n); } @Override public void visit(NodeTraversal t, Node n, Node parent) { // Do the checks when 'n' is the block node and 'parent' is the function // node, so that getControlFlowGraph will return the graph inside // the function, rather than the graph of the enclosing scope. if (hasReturnDeclaredNullable(n) && !canReturnNull(t.getControlFlowGraph())) { String fnName = NodeUtil.getNearestFunctionName(parent); if (fnName != null && !fnName.isEmpty()) { compiler.report(t.makeError(parent, NULLABLE_RETURN_WITH_NAME, fnName)); } else { compiler.report(t.makeError(parent, NULLABLE_RETURN)); } } } /** @return whether the blockNode contains only a single "throw" child node. */ private static boolean hasSingleThrow(Node blockNode) { if (blockNode.getChildCount() == 1 && blockNode.getFirstChild().getType() == Token.THROW) { // Functions consisting of a single "throw FOO" can be actually abstract, // so do not check their return type nullability. return true; } return false; } /** * @return True if n is a function node which is explicitly annotated as returning a nullable * type, other than {?}. */ private static boolean isReturnTypeNullable(Node n) { if (n == null || !n.isFunction()) { return false; } FunctionType functionType = n.getJSType().toMaybeFunctionType(); if (functionType == null) { // If the JSDoc declares a non-function type on a function node, we still shouldn't crash. return false; } JSType returnType = functionType.getReturnType(); if (returnType == null || returnType.isUnknownType() || !returnType.isNullable()) { return false; } JSDocInfo info = NodeUtil.getBestJSDocInfo(n); return info != null && info.hasReturnType(); } /** @return True if the given ControlFlowGraph could return null. */ public static boolean canReturnNull(ControlFlowGraph<Node> graph) { CheckPathsBetweenNodes<Node, ControlFlowGraph.Branch> test = new CheckPathsBetweenNodes<>( graph, graph.getEntry(), graph.getImplicitReturn(), NULLABLE_RETURN_PREDICATE, Predicates.<DiGraphEdge<Node, ControlFlowGraph.Branch>>alwaysTrue()); return test.somePathsSatisfyPredicate(); } /** * @return True if the node represents a nullable value. Essentially, this is just * n.getJSType().isNullable(), but for purposes of this pass, the expression {@code x || null} * is considered nullable even if x is always truthy. This often happens with expressions like * {@code arr[i] || null}: The compiler doesn't know that arr[i] can be undefined. */ private static boolean isNullable(Node n) { return n.getJSType().isNullable() || (n.isOr() && n.getLastChild().isNull()); } @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return true; } @Override public void process(Node externs, Node root) { NodeTraversal.traverseEs6(compiler, root, this); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { NodeTraversal.traverseEs6(compiler, originalRoot, this); } }