@Test public void canUseModuleInternalTypedefsInJsDoc() { CompilerUtil compiler = createCompiler(path("foo.js")); compiler.compile( createSourceFile( path("foo.js"), "/** @typedef {{x: number}} */", "var Variable;", "", "/**", " * @param {Variable} a .", " * @param {Variable} b .", " * @return {Variable} .", " */", "exports.add = function(a, b) {", " return {x: a.x + b.x};", "};")); Scope scope = compiler.getCompiler().getTopScope(); Var var = scope.getVar("module$foo"); JSType type = var.getInitialValue().getJSType().toObjectType().getPropertyType("add"); assertTrue(type.isFunctionType()); JSDocInfo info = type.getJSDocInfo(); Node node = info.getTypeNodes().iterator().next(); assertTrue(node.isString()); assertEquals("$jscomp.scope.Variable", node.getString()); assertEquals("Variable", node.getProp(Node.ORIGINALNAME_PROP)); }
@Test public void exportedInternalVarInheritsJsDocInfo() { CompilerUtil compiler = createCompiler(path("foo.js")); compiler.compile( createSourceFile( path("foo.js"), "/**", " * @constructor", " */", "var Greeter = function(){};", "/**", " * @param {string} name .", " * @return {string} .", " */", "Greeter.prototype.sayHi = function(name) {", " return 'Hello, ' + name;", "};", "", "exports.Greeter = Greeter")); JSType exportedGreeter = compiler .getCompiler() .getTopScope() .getVar("module$foo") .getType() .findPropertyType("Greeter"); assertTrue(exportedGreeter.isConstructor()); }
@Test public void savesOriginalTypeNameInJsDoc() { CompilerUtil compiler = createCompiler(path("foo.js")); compiler.compile( createSourceFile( path("foo.js"), "/** @constructor */", "var Builder = function(){};", "/** @return {!Builder} . */", "Builder.prototype.returnThis = function() { return this; };", "exports.Builder = Builder")); Scope scope = compiler.getCompiler().getTopScope(); Var var = scope.getVar("module$foo"); JSType type = var.getInitialValue().getJSType().findPropertyType("Builder"); assertTrue(type.isConstructor()); type = type.toObjectType().getTypeOfThis(); assertEquals("$jscomp.scope.Builder", type.toString()); type = type.toObjectType().getPropertyType("returnThis"); assertTrue(type.toString(), type.isFunctionType()); JSDocInfo info = type.getJSDocInfo(); assertNotNull(info); Node node = getOnlyElement(info.getTypeNodes()); assertEquals(Token.BANG, node.getType()); node = node.getFirstChild(); assertTrue(node.isString()); assertEquals("$jscomp.scope.Builder", node.getString()); assertEquals("Builder", node.getProp(Node.ORIGINALNAME_PROP)); }
@Override public boolean apply(JSType type) { // TODO(user): Doing an instanceof check here is too // restrictive as (Date,Error) is, for instance, an object type // even though its implementation is a UnionType. Would need to // create interfaces JSType, ObjectType, FunctionType etc and have // separate implementation instead of the class hierarchy, so that // union types can also be object types, etc. if (!type.restrictByNotNullOrUndefined().isSubtype(typeRegistry.getNativeType(OBJECT_TYPE))) { reportWarning(THIS_TYPE_NON_OBJECT, type.toString()); return false; } return true; }
private String getTypeAnnotation(Node node) { // Only add annotations for things with JSDoc, or function literals. JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(node); if (jsdoc == null && !node.isFunction()) { return ""; } JSType type = node.getJSType(); if (type == null) { return ""; } else if (type.isFunctionType()) { return getFunctionAnnotation(node); } else if (type.isEnumType()) { return "/** @enum {" + type.toMaybeEnumType().getElementsType().toAnnotationString() + "} */\n"; } else if (!type.isUnknownType() && !type.isEmptyType() && !type.isVoidType() && !type.isFunctionPrototypeType()) { return "/** @type {" + node.getJSType().toAnnotationString() + "} */\n"; } else { return ""; } }
/** Gets the type of {@code this} in the current scope. */ @Override public JSType getTypeOfThis() { if (isGlobal()) { return ObjectType.cast(rootNode.getJSType()); } Preconditions.checkState(rootNode.isFunction()); JSType nodeType = rootNode.getJSType(); if (nodeType != null && nodeType.isFunctionType()) { return nodeType.toMaybeFunctionType().getTypeOfThis(); } else { // Executed when the current scope has not been typechecked. return null; } }
private Set<JSType> getTypesToSkipForTypeNonUnion(JSType type) { Set<JSType> types = Sets.newHashSet(); JSType skipType = type; while (skipType != null) { types.add(skipType); ObjectType objSkipType = skipType.toObjectType(); if (objSkipType != null) { skipType = objSkipType.getImplicitPrototype(); } else { break; } } return types; }
/** * @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(); }
/** * Add concrete types for autoboxing types if necessary. The concrete type system does not track * native types, like string, so add them if they are present in the JSType for the node. */ private ConcreteType maybeAddAutoboxes(ConcreteType cType, Node node, String prop) { JSType jsType = node.getJSType(); if (jsType == null) { return cType; } else if (jsType.isUnknownType()) { for (JSTypeNative nativeType : nativeTypes) { ConcreteType concrete = tt.getConcreteInstance(tt.getTypeRegistry().getNativeObjectType(nativeType)); if (concrete != null && !concrete.getPropertyType(prop).isNone()) { cType = cType.unionWith(concrete); } } return cType; } return maybeAddAutoboxes(cType, jsType, prop); }
private ConcreteType maybeAddAutoboxes(ConcreteType cType, JSType jsType, String prop) { jsType = jsType.restrictByNotNullOrUndefined(); if (jsType instanceof UnionType) { for (JSType alt : ((UnionType) jsType).getAlternates()) { return maybeAddAutoboxes(cType, alt, prop); } } if (jsType.autoboxesTo() != null) { JSType autoboxed = jsType.autoboxesTo(); return cType.unionWith(tt.getConcreteInstance((ObjectType) autoboxed)); } else if (jsType.unboxesTo() != null) { return cType.unionWith(tt.getConcreteInstance((ObjectType) jsType)); } return cType; }
@Test public void canReferenceConstructorDefinedInTheGlobalScope() { CompilerUtil compiler = createCompiler(path("x/bar.js")); compiler.compile( createSourceFile(path("x/foo.js"), "/** @constructor */", "function Foo() {}"), createSourceFile( path("x/bar.js"), "/** @type {function(new: Foo)} */", "exports.Foo = Foo;")); Scope scope = compiler.getCompiler().getTopScope(); Var var = scope.getVar("module$x$bar"); JSType type = var.getInitialValue().getJSType().findPropertyType("Foo"); assertTrue(type.isConstructor()); type = type.toObjectType().getTypeOfThis(); assertEquals("Foo", type.toString()); }
@Override public Iterable<JSType> getTypeAlternatives(JSType type) { if (type.isUnionType()) { return ((UnionType) type).getAlternates(); } else { ObjectType objType = type.toObjectType(); if (objType != null && objType.getConstructor() != null && objType.getConstructor().isInterface()) { List<JSType> list = Lists.newArrayList(); for (FunctionType impl : registry.getDirectImplementors(objType)) { list.add(impl.getInstanceType()); } return list; } else { return null; } } }
@Override public boolean isInvalidatingType(JSType type) { if (type == null || invalidatingTypes.contains(type) || type.isUnknownType() /* unresolved types */) { return true; } ObjectType objType = ObjectType.cast(type); return objType != null && !objType.hasReferenceName(); }
@Override public JSType getInstanceFromPrototype(JSType type) { if (type.isFunctionPrototypeType()) { FunctionPrototypeType prototype = (FunctionPrototypeType) type; FunctionType owner = prototype.getOwnerFunction(); if (owner.isConstructor() || owner.isInterface()) { return ((FunctionPrototypeType) type).getOwnerFunction().getInstanceType(); } } return null; }
/** * Infer the role of the function (whether it's a constructor or interface) and what it inherits * from in JSDocInfo. */ FunctionTypeBuilder inferInheritance(@Nullable JSDocInfo info) { if (info != null) { isConstructor = info.isConstructor(); isInterface = info.isInterface(); // base type if (info.hasBaseType()) { if (isConstructor || isInterface) { JSType maybeBaseType = info.getBaseType().evaluate(scope, typeRegistry); if (maybeBaseType != null && maybeBaseType.setValidator(new ExtendedTypeValidator())) { baseType = (ObjectType) maybeBaseType; } } else { reportWarning(EXTENDS_WITHOUT_TYPEDEF, fnName); } } // implemented interfaces if (isConstructor || isInterface) { implementedInterfaces = Lists.newArrayList(); for (JSTypeExpression t : info.getImplementedInterfaces()) { JSType maybeInterType = t.evaluate(scope, typeRegistry); if (maybeInterType != null && maybeInterType.setValidator(new ImplementedTypeValidator())) { implementedInterfaces.add((ObjectType) maybeInterType); } } if (baseType != null) { JSType maybeFunctionType = baseType.getConstructor(); if (maybeFunctionType instanceof FunctionType) { FunctionType functionType = baseType.getConstructor(); Iterables.addAll(implementedInterfaces, functionType.getImplementedInterfaces()); } } } else if (info.getImplementedInterfaceCount() > 0) { reportWarning(IMPLEMENTS_WITHOUT_CONSTRUCTOR, fnName); } } return this; }
@Override public ImmutableSet<JSType> getTypesToSkipForType(JSType type) { type = type.restrictByNotNullOrUndefined(); if (type instanceof UnionType) { Set<JSType> types = Sets.newHashSet(type); for (JSType alt : ((UnionType) type).getAlternates()) { types.addAll(getTypesToSkipForTypeNonUnion(type)); } return ImmutableSet.copyOf(types); } return ImmutableSet.copyOf(getTypesToSkipForTypeNonUnion(type)); }
@Test public void canReferenceConstructorExportedByAnotherModule() { CompilerUtil compiler = createCompiler(path("x/foo.js"), path("x/bar.js")); compiler.compile( createSourceFile(path("x/foo.js"), "/** @constructor */", "exports.Foo = function(){};"), createSourceFile( path("x/bar.js"), "var foo = require('./foo');", "/** @type {function(new: foo.Foo)} */", "exports.Foo = foo.Foo;")); Scope scope = compiler.getCompiler().getTopScope(); Var var = scope.getVar("module$x$bar"); JSType type = var.getInitialValue().getJSType().findPropertyType("Foo"); assertTrue(type.isConstructor()); type = type.toObjectType().getTypeOfThis(); assertEquals("module$x$foo.Foo", type.toString()); }
@Override public JSType caseTemplateType(TemplateType type) { if (replacements.hasTemplateKey(type)) { if (hasVisitedType(type) || !replacements.hasTemplateType(type)) { // If we have already encountered this TemplateType during replacement // (i.e. there is a reference loop), or there is no JSType substitution // for the TemplateType, return the TemplateType type itself. return type; } else { JSType replacement = replacements.getTemplateType(type); visitedTypes.push(type); JSType visitedReplacement = replacement.visit(this); visitedTypes.pop(); return visitedReplacement; } } else { return type; } }
/** * Creates a JSDoc-suitable String representation the type of a parameter. * * @param parameterNode The parameter node. */ private String getParameterNodeJSDocType(Node parameterNode) { JSType parameterType = parameterNode.getJSType(); String typeString; // Emit unknown types as '*' (AllType) since '?' (UnknownType) is not // a valid JSDoc type. if (parameterType.isUnknownType()) { typeString = "*"; } else { // Fix-up optional and vararg parameters to match JSDoc type language if (parameterNode.isOptionalArg()) { typeString = parameterType.restrictByNotNullOrUndefined().toAnnotationString() + "="; } else if (parameterNode.isVarArgs()) { typeString = "..." + parameterType.restrictByNotNullOrUndefined().toAnnotationString(); } else { typeString = parameterType.toAnnotationString(); } } return typeString; }
@Override public ObjectType getTypeWithProperty(String field, JSType type) { if (!(type instanceof ObjectType)) { if (type.autoboxesTo() != null) { type = type.autoboxesTo(); } else { return null; } } // Ignore the prototype itself at all times. if ("prototype".equals(field)) { return null; } // We look up the prototype chain to find the highest place (if any) that // this appears. This will make references to overriden properties look // like references to the initial property, so they are renamed alike. ObjectType foundType = null; ObjectType objType = ObjectType.cast(type); while (objType != null && objType.getImplicitPrototype() != objType) { if (objType.hasOwnProperty(field)) { foundType = objType; } objType = objType.getImplicitPrototype(); } // If the property does not exist on the referenced type but the original // type is an object type, see if any subtype has the property. if (foundType == null) { ObjectType maybeType = ObjectType.cast(registry.getGreatestSubtypeWithProperty(type, field)); // getGreatestSubtypeWithProperty does not guarantee that the property // is defined on the returned type, it just indicates that it might be, // so we have to double check. if (maybeType != null && maybeType.hasOwnProperty(field)) { foundType = maybeType; } } return foundType; }
/** Infer the return type from JSDocInfo. */ FunctionTypeBuilder inferReturnType(@Nullable JSDocInfo info) { if (info != null && info.hasReturnType()) { returnType = info.getReturnType().evaluate(scope, typeRegistry); returnTypeInferred = false; } if (templateTypeName != null && returnType != null && returnType.restrictByNotNullOrUndefined().isTemplateType()) { reportError(TEMPLATE_TYPE_EXPECTED, fnName); } return this; }
/** Invalidates the given type, so that no properties on it will be renamed. */ private void addInvalidatingType(JSType type) { type = type.restrictByNotNullOrUndefined(); if (type instanceof UnionType) { for (JSType alt : ((UnionType) type).getAlternates()) { addInvalidatingType(alt); } return; } typeSystem.addInvalidatingType(type); ObjectType objType = ObjectType.cast(type); if (objType != null && objType.getImplicitPrototype() != null) { typeSystem.addInvalidatingType(objType.getImplicitPrototype()); } }
@Override public boolean apply(JSType type) { ObjectType objectType = ObjectType.cast(type); if (objectType == null) { reportWarning(EXTENDS_NON_OBJECT, fnName, type.toString()); } else if (objectType.isUnknownType() && // If this has a supertype that hasn't been resolved yet, // then we can assume this type will be ok once the super // type resolves. (objectType.getImplicitPrototype() == null || objectType.getImplicitPrototype().isResolved())) { reportWarning(RESOLVED_TAG_EMPTY, "@extends", fnName); } else { return true; } return false; }
@Override public JSType caseObjectType(ObjectType type) { JSType arrayType = getNativeType(ARRAY_TYPE); return arrayType.isSubtype(type) ? arrayType : null; }
@Override protected JSType caseTopType(JSType topType) { return topType.isAllType() ? getNativeType(ARRAY_TYPE) : topType; }
/** * Returns whether two nodes are equivalent, taking into account the template parameters that were * provided to this matcher. If the template comparison node is a parameter node, then only the * types of the node must match. Otherwise, the node must be equal and the child nodes must be * equivalent according to the same function. This differs from the built in Node equivalence * function with the special comparison. */ private boolean matchesNode(Node template, Node ast) { if (isTemplateParameterNode(template)) { int paramIndex = (int) (template.getDouble()); Node previousMatch = paramNodeMatches.get(paramIndex); if (previousMatch != null) { // If this named node has already been matched against, make sure all // subsequent usages of the same named node are equivalent. return ast.isEquivalentTo(previousMatch); } // Only the types need to match for the template parameters, which allows // the template function to express arbitrary expressions. JSType templateType = template.getJSType(); Preconditions.checkNotNull(templateType, "null template parameter type."); // TODO(johnlenz): We shouldn't spend time checking template whose // types whose definitions aren't included (NoResolvedType). Alternately // we should treat them as "unknown" and perform loose matches. if (templateType.isNoResolvedType()) { return false; } MatchResult matchResult = typeMatchingStrategy.match(templateType, ast.getJSType()); isLooseMatch = matchResult.isLooseMatch(); boolean isMatch = matchResult.isMatch(); if (isMatch && previousMatch == null) { paramNodeMatches.set(paramIndex, ast); } return isMatch; } else if (isTemplateLocalNameNode(template)) { // If this template name node was already matched against, then make sure // all subsequent usages of the same template name node are equivalent in // the matched code. // For example, this code will handle the case: // function template() { // var a = 'str'; // fn(a); // } // // will only match test code: // var b = 'str'; // fn(b); // // but it will not match: // var b = 'str'; // fn('str'); int paramIndex = (int) (template.getDouble()); boolean previouslyMatched = this.localVarMatches.get(paramIndex) != null; if (previouslyMatched) { // If this named node has already been matched against, make sure all // subsequent usages of the same named node are equivalent. return ast.getString().equals(this.localVarMatches.get(paramIndex)); } else { this.localVarMatches.set(paramIndex, ast.getString()); } } // Template and AST shape has already been checked, but continue look for // other template variables (parameters and locals) that must be checked. Node templateChild = template.getFirstChild(); Node astChild = ast.getFirstChild(); while (templateChild != null) { if (!matchesNode(templateChild, astChild)) { return false; } templateChild = templateChild.getNext(); astChild = astChild.getNext(); } return true; }
/** Infer the parameter types from the list of argument names and the doc info. */ FunctionTypeBuilder inferParameterTypes(@Nullable Node argsParent, @Nullable JSDocInfo info) { if (argsParent == null) { if (info == null) { return this; } else { return inferParameterTypes(info); } } // arguments Node oldParameterType = null; if (parametersNode != null) { oldParameterType = parametersNode.getFirstChild(); } FunctionParamBuilder builder = new FunctionParamBuilder(typeRegistry); boolean warnedAboutArgList = false; Set<String> allJsDocParams = (info == null) ? Sets.<String>newHashSet() : Sets.newHashSet(info.getParameterNames()); boolean foundTemplateType = false; for (Node arg : argsParent.children()) { String argumentName = arg.getString(); allJsDocParams.remove(argumentName); // type from JSDocInfo JSType parameterType = null; boolean isOptionalParam = isOptionalParameter(arg, info); boolean isVarArgs = isVarArgsParameter(arg, info); if (info != null && info.hasParameterType(argumentName)) { parameterType = info.getParameterType(argumentName).evaluate(scope, typeRegistry); } else if (oldParameterType != null && oldParameterType.getJSType() != null) { parameterType = oldParameterType.getJSType(); isOptionalParam = oldParameterType.isOptionalArg(); isVarArgs = oldParameterType.isVarArgs(); } else { parameterType = typeRegistry.getNativeType(UNKNOWN_TYPE); } if (templateTypeName != null && parameterType.restrictByNotNullOrUndefined().isTemplateType()) { if (foundTemplateType) { reportError(TEMPLATE_TYPE_DUPLICATED, fnName); } foundTemplateType = true; } warnedAboutArgList |= addParameter(builder, parameterType, warnedAboutArgList, isOptionalParam, isVarArgs); if (oldParameterType != null) { oldParameterType = oldParameterType.getNext(); } } if (templateTypeName != null && !foundTemplateType) { reportError(TEMPLATE_TYPE_EXPECTED, fnName); } for (String inexistentName : allJsDocParams) { reportWarning(INEXISTANT_PARAM, inexistentName, fnName); } parametersNode = builder.build(); return this; }
@Override public void addInvalidatingType(JSType type) { checkState(!type.isUnionType()); invalidatingTypes.add(type); }
@Override public boolean isTypeToSkip(JSType type) { return type.isEnumType() || (type.autoboxesTo() != null); }
@Override public JSType restrictByNotNullOrUndefined(JSType type) { return type.restrictByNotNullOrUndefined(); }