diff --git a/src/com/google/javascript/jscomp/DiagnosticGroups.java b/src/com/google/javascript/jscomp/DiagnosticGroups.java index 994198951f7..b50d150371e 100644 --- a/src/com/google/javascript/jscomp/DiagnosticGroups.java +++ b/src/com/google/javascript/jscomp/DiagnosticGroups.java @@ -355,7 +355,7 @@ public DiagnosticGroup forName(String name) { // NewTypeInference.INEXISTENT_PROPERTY, // NewTypeInference.INVALID_ARGUMENT_TYPE, // NewTypeInference.INVALID_CAST, - NewTypeInference.INVALID_INDEX_TYPE, +// NewTypeInference.INVALID_INDEX_TYPE, NewTypeInference.INVALID_INFERRED_RETURN_TYPE, NewTypeInference.INVALID_OBJLIT_PROPERTY_TYPE, // NewTypeInference.INVALID_OPERAND_TYPE, diff --git a/src/com/google/javascript/jscomp/NewTypeInference.java b/src/com/google/javascript/jscomp/NewTypeInference.java index 83bb1dc7fda..db788b3451c 100644 --- a/src/com/google/javascript/jscomp/NewTypeInference.java +++ b/src/com/google/javascript/jscomp/NewTypeInference.java @@ -2978,7 +2978,8 @@ private boolean mayWarnAboutInexistentProp(Node propAccessNode, } if (recvType.isUnknown() || recvType.isTrueOrTruthy() || recvType.isLoose() - || allowPropertyOnSubtypes && recvType.mayContainUnknownObject()) { + || (allowPropertyOnSubtypes + && (recvType.mayContainUnknownObject() || recvType.isIObject()))) { if (symbolTable.isPropertyDefined(pname)) { return false; } diff --git a/src/com/google/javascript/jscomp/newtypes/JSType.java b/src/com/google/javascript/jscomp/newtypes/JSType.java index f3e934f0699..6476fac7e10 100644 --- a/src/com/google/javascript/jscomp/newtypes/JSType.java +++ b/src/com/google/javascript/jscomp/newtypes/JSType.java @@ -1282,6 +1282,11 @@ public boolean isNonClassyObject() { return nt != null && !nt.isClassy(); } + public boolean isIObject() { + NominalType nt = getNominalTypeIfSingletonObj(); + return nt != null && nt.isIObject(); + } + public boolean isInterfaceDefinition() { FunctionType ft = getFunTypeIfSingletonObj(); return ft != null && ft.isInterfaceDefinition(); diff --git a/src/com/google/javascript/jscomp/newtypes/JSTypeCreatorFromJSDoc.java b/src/com/google/javascript/jscomp/newtypes/JSTypeCreatorFromJSDoc.java index c7962428d0a..c9fe01a6cf7 100644 --- a/src/com/google/javascript/jscomp/newtypes/JSTypeCreatorFromJSDoc.java +++ b/src/com/google/javascript/jscomp/newtypes/JSTypeCreatorFromJSDoc.java @@ -414,12 +414,22 @@ private JSType getNamedTypeHelper( case "Function": checkInvalidGenericsInstantiation(n); return maybeMakeNullable(this.commonTypes.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() - ? this.commonTypes.getTopDict() : this.commonTypes.getTopObject()); + case "Object": { + JSType result; + if (n.hasChildren()) { + // We consider a parameterized Object<...> as an alias of IObject. + NominalType iobject = this.commonTypes.getIObjectType(); + if (iobject == null) { + // Can happen when using old externs. + return this.commonTypes.UNKNOWN; + } + result = getNominalTypeHelper( + iobject.getRawNominalType(), n, registry, outerTypeParameters); + } else { + result = this.commonTypes.getTopObject(); + } + return maybeMakeNullable(result); + } default: return lookupTypeByName(typeName, n, registry, outerTypeParameters); } @@ -534,14 +544,13 @@ private JSType getNominalTypeHelper(RawNominalType rawType, Node n, ImmutableList.Builder typeList = ImmutableList.builder(); if (n.hasChildren()) { // Compute instantiation of polymorphic class/interface. - Preconditions.checkState(n.getFirstChild().isBlock(), n); + Preconditions.checkState(n.getFirstChild().isNormalBlock(), n); for (Node child : n.getFirstChild().children()) { - typeList.add( - getTypeFromCommentHelper(child, registry, outerTypeParameters)); + typeList.add(getTypeFromCommentHelper(child, registry, outerTypeParameters)); } } - ImmutableList typeArguments = typeList.build(); - ImmutableList typeParameters = rawType.getTypeParameters(); + List typeArguments = typeList.build(); + List typeParameters = rawType.getTypeParameters(); int typeArgsSize = typeArguments.size(); int typeParamsSize = typeParameters.size(); if (typeArgsSize != typeParamsSize) { @@ -555,16 +564,15 @@ private JSType getNominalTypeHelper(RawNominalType rawType, Node n, String.valueOf(typeParamsSize), String.valueOf(typeArgsSize))); } - return maybeMakeNullable(JSType.fromObjectType(ObjectType.fromNominalType( - uninstantiated.instantiateGenerics( - fixLengthOfTypeList(typeParameters.size(), typeArguments))))); + typeArguments = fixLengthOfTypeList(typeParameters.size(), typeArguments); + NominalType instantiated = uninstantiated.instantiateGenerics(typeArguments); + return maybeMakeNullable(JSType.fromObjectType(ObjectType.fromNominalType(instantiated))); } return maybeMakeNullable(JSType.fromObjectType(ObjectType.fromNominalType( uninstantiated.instantiateGenerics(typeArguments)))); } - private List fixLengthOfTypeList( - int desiredLength, List typeList) { + private List fixLengthOfTypeList(int desiredLength, List typeList) { int length = typeList.size(); if (length == desiredLength) { return typeList; diff --git a/src/com/google/javascript/jscomp/newtypes/ObjectType.java b/src/com/google/javascript/jscomp/newtypes/ObjectType.java index c769f93ede5..ec2bcf0070f 100644 --- a/src/com/google/javascript/jscomp/newtypes/ObjectType.java +++ b/src/com/google/javascript/jscomp/newtypes/ObjectType.java @@ -682,7 +682,7 @@ private boolean compareRecordTypeToIObject( if (keyType.isNumber() && Ints.tryParse(pname) == null) { return false; } - if (!keyType.isNumber() && !keyType.isString()) { + if (!keyType.isNumber() && !keyType.isString() && !keyType.isUnknown()) { return false; } if (!ptype.isSubtypeOf(valueType, subSuperMap)) { diff --git a/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java b/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java index 7779d1537cf..f2479526a30 100644 --- a/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java +++ b/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java @@ -2017,12 +2017,13 @@ private Node parseTopLevelTypeExpression(JsDocToken token) { * TypeExpressionList := TopLevelTypeExpression * | TopLevelTypeExpression ',' TypeExpressionList */ - private Node parseTypeExpressionList(JsDocToken token) { + private Node parseTypeExpressionList(String typeName, JsDocToken token) { Node typeExpr = parseTopLevelTypeExpression(token); if (typeExpr == null) { return null; } Node typeList = IR.block(); + int numTypeExprs = 1; typeList.addChildToBack(typeExpr); while (match(JsDocToken.COMMA)) { next(); @@ -2031,8 +2032,13 @@ private Node parseTypeExpressionList(JsDocToken token) { if (typeExpr == null) { return null; } + numTypeExprs++; typeList.addChildToBack(typeExpr); } + if (typeName.equals("Object") && numTypeExprs == 1) { + // Unlike other generic types, Object means Object, not Object. + typeList.addChildToFront(newNode(Token.QMARK)); + } return typeList; } @@ -2161,7 +2167,7 @@ private Node parseTypeName(JsDocToken token) { if (match(JsDocToken.LEFT_ANGLE)) { next(); skipEOLs(); - Node memberType = parseTypeExpressionList(next()); + Node memberType = parseTypeExpressionList(typeName, next()); if (memberType != null) { typeNameNode.addChildToFront(memberType); diff --git a/test/com/google/javascript/jscomp/Es6InlineTypesNotYetParsedTest.java b/test/com/google/javascript/jscomp/Es6InlineTypesNotYetParsedTest.java index 5556356df92..837318976f4 100644 --- a/test/com/google/javascript/jscomp/Es6InlineTypesNotYetParsedTest.java +++ b/test/com/google/javascript/jscomp/Es6InlineTypesNotYetParsedTest.java @@ -115,7 +115,7 @@ public void testParameterizedType() { assertSource("/** @type {Object.} */ var s;") .transpilesTo("var s: Object;"); assertSource("/** @type {Object.} */ var s;") - .transpilesTo("var s: Object;"); + .transpilesTo("var s: Object;"); } public void testParameterizedTypeWithVoid() throws Exception { diff --git a/test/com/google/javascript/jscomp/NewTypeInferenceTest.java b/test/com/google/javascript/jscomp/NewTypeInferenceTest.java index 8fc7325d468..7c98d929767 100644 --- a/test/com/google/javascript/jscomp/NewTypeInferenceTest.java +++ b/test/com/google/javascript/jscomp/NewTypeInferenceTest.java @@ -16350,7 +16350,7 @@ public void testRecursiveStructuralInterfaces() { "}")); } - public void testIObjectAccesses() { + public void testIObjectBracketAccesses() { typeCheck(LINE_JOINER.join( "function f(/** !IObject */ x) {", " return x['asdf'];", @@ -16445,6 +16445,37 @@ public void testIObjectAccesses() { NewTypeInference.MISTYPED_ASSIGN_RHS); } + // A dot access on IObject is not typed as V. + // It is either declared separately with its own type (eg, when Foo implements IObject and + // has some extra properties), or it is a type error. + public void testIObjectDotAccesses() { + typeCheck(LINE_JOINER.join( + "function f(/** !IObject */ x) {", + " return x.hasOwnProperty('test');", + "}")); + + typeCheck(LINE_JOINER.join( + "function g(x) {", + " x.foobar = 123;", + "}", + "function f(/** !IObject */ x) {", + " return x.foobar;", + "}"), + NewTypeInference.INEXISTENT_PROPERTY); + + typeCheck(LINE_JOINER.join( + "/** @constructor */", + "function Foo() {", + " this.foobar = 123;", + "}", + "function f(/** !IObject */ x) {", + " return x.foobar;", + "}"), + NewTypeInference.INEXISTENT_PROPERTY); + + typeCheck("var /** !IObject */ x = { 'abs': '', '%': ''};"); + } + public void testIObjectSubtyping() { typeCheck(LINE_JOINER.join( "function f(/** !IObject */ x) {}", @@ -17930,6 +17961,15 @@ public void testPropertyCheckingCompatibility() { " this.prop = 123;", "}")); + typeCheck(LINE_JOINER.join( + "function f(/** !IObject */ x) {", + " return x.prop + 1;", + "}", + "/** @constructor */", + "function Bar() {", + " this.prop = 123;", + "}")); + typeCheck(LINE_JOINER.join( "/** @constructor */", "function Foo() {}", @@ -18643,4 +18683,63 @@ public void testConstInferenceDependsOnOrderOfScopeTraversing() { " }", "}")); } + + // We currently consider Object to be the same as IObject. + // Since we may change that in the future, they are tested separately. + public void testParameterizedObject() { + typeCheck(LINE_JOINER.join( + "function f(/** !Object */ x) {", + " return x.hasOwnProperty('test');", + "}")); + + typeCheck(LINE_JOINER.join( + "function g(x) {", + " x.foobar = 123;", + "}", + "function f(/** !Object */ x) {", + " return x.foobar;", + "}"), + NewTypeInference.INEXISTENT_PROPERTY); + + typeCheck(LINE_JOINER.join( + "/** @constructor */", + "function Foo() {", + " this.foobar = 123;", + "}", + "function f(/** !Object */ x) {", + " return x.foobar;", + "}"), + NewTypeInference.INEXISTENT_PROPERTY); + + typeCheck("var /** !Object */ x = { 'abs': '', '%': ''};"); + + typeCheck(LINE_JOINER.join( + "function f(/** !Object */ x) {", + " return x['asdf'];", + "}"), + NewTypeInference.INVALID_INDEX_TYPE); + + typeCheck(LINE_JOINER.join( + "function f(/** !Object */ x) {", + " x['asdf'] = 123;", + "}"), + NewTypeInference.INVALID_INDEX_TYPE); + + typeCheck(LINE_JOINER.join( + "function f(/** !Object */ x, /** number */ i) {", + " x[i] - 123;", + "}"), + NewTypeInference.INVALID_OPERAND_TYPE); + + typeCheck(LINE_JOINER.join( + "function f(/** !Object */ x) {", + " var /** string */ s = x['asdf'];", + "}"), + NewTypeInference.MISTYPED_ASSIGN_RHS); + + typeCheck(LINE_JOINER.join( + "function f(/** !Object */ x) {", + " return x[{a: 123}] + x['sadf'];", + "}")); + } }