From 402f52a00812fb2ff73621df29265fdf97560eb4 Mon Sep 17 00:00:00 2001 From: nickreid Date: Thu, 28 Mar 2019 12:27:49 -0700 Subject: [PATCH] Modifies property override validation to cover purely prototypal inheritance. This is required for validation of class-side overrides, to ensure that static members of an ES6 syntax subclass are compatible with any superclass definitions. This applied to ES6 syntax interfaces too, as well as any type with a known prototypal heritage. The errors generated by this check are included in a new diagnostic group 'checkPrototypalTypes'. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=240827487 --- .../javascript/jscomp/DiagnosticGroups.java | 25 +- .../jscomp/FunctionTypeBuilder.java | 7 +- .../google/javascript/jscomp/TypeCheck.java | 181 +++++++++++--- .../javascript/rhino/jstype/ObjectType.java | 29 +++ .../jscomp/TypeCheckNoTranspileTest.java | 235 ++++++++++++++++-- 5 files changed, 411 insertions(+), 66 deletions(-) diff --git a/src/com/google/javascript/jscomp/DiagnosticGroups.java b/src/com/google/javascript/jscomp/DiagnosticGroups.java index 8917d7766c1..b0ccab189f4 100644 --- a/src/com/google/javascript/jscomp/DiagnosticGroups.java +++ b/src/com/google/javascript/jscomp/DiagnosticGroups.java @@ -120,6 +120,7 @@ public DiagnosticGroup forName(String name) { static final String DIAGNOSTIC_GROUP_NAMES = "accessControls, " + "ambiguousFunctionDecl, " + + "checkPrototypalTypes, " + "checkRegExp, " + "checkTypes, " + "checkVars, " @@ -146,27 +147,27 @@ public DiagnosticGroup forName(String name) { + "missingProvide, " + "missingRequire, " + "missingReturn, " + + "missingSourcesWarnings, " + "moduleLoad, " + "msgDescriptions, " + "newCheckTypes, " + "nonStandardJsDocs, " - + "missingSourcesWarnings, " + "polymer, " + "reportUnknownTypes, " - + "suspiciousCode, " + "strictCheckTypes, " + "strictMissingProperties, " + "strictModuleDepCheck, " + "strictPrimitiveOperators, " + + "suspiciousCode, " + "typeInvalidation, " + "undefinedNames, " + "undefinedVars, " + + "underscore, " + "unknownDefines, " + "unusedLocalVariables, " + "unusedPrivateMembers, " - + "uselessCode, " + "useOfGoogBase, " - + "underscore, " + + "uselessCode, " + "untranspilableFeatures," + "visibility"; @@ -269,6 +270,7 @@ public DiagnosticGroup forName(String name) { DiagnosticGroups.registerGroup( "missingOverride", TypeCheck.HIDDEN_INTERFACE_PROPERTY, + TypeCheck.HIDDEN_PROTOTYPAL_SUPERTYPE_PROPERTY, TypeCheck.HIDDEN_SUPERCLASS_PROPERTY); public static final DiagnosticGroup MISSING_PROPERTIES = @@ -320,11 +322,16 @@ public DiagnosticGroup forName(String name) { FunctionTypeBuilder.ALL_DIAGNOSTICS, DiagnosticGroups.GLOBAL_THIS); - // TODO(b/112639311): Populate this group with appropriate diagnostics. - // This group isn't registered because it has no associated suppression. It's only ever referenced - // in Java code. - public static final DiagnosticGroup CHECK_STATIC_OVERRIDES = - new DiagnosticGroup("checkStaticOverrides_unregistered", UNUSED); + public static final DiagnosticGroup CHECK_PROTOTYPAL_TYPES = + DiagnosticGroups.registerGroup( + "checkPrototypalTypes", + TypeCheck.UNKNOWN_PROTOTYPAL_OVERRIDE, + TypeCheck.HIDDEN_PROTOTYPAL_SUPERTYPE_PROPERTY, + TypeCheck.HIDDEN_PROTOTYPAL_SUPERTYPE_PROPERTY_MISMATCH); + + // This group exists for the J2CL team to suppress the associated diagnostics using Java code + // rather than `@suppress` annotations. + public static final DiagnosticGroup CHECK_STATIC_OVERRIDES = CHECK_PROTOTYPAL_TYPES; // Run the new type inference, but omit many warnings that are not // found by the old type checker. This makes migration to NTI more manageable. diff --git a/src/com/google/javascript/jscomp/FunctionTypeBuilder.java b/src/com/google/javascript/jscomp/FunctionTypeBuilder.java index 1622e794d61..5259ab17cde 100644 --- a/src/com/google/javascript/jscomp/FunctionTypeBuilder.java +++ b/src/com/google/javascript/jscomp/FunctionTypeBuilder.java @@ -424,7 +424,7 @@ FunctionTypeBuilder inferInheritance( @Nullable JSDocInfo info, @Nullable ObjectType classExtendsType) { if (info != null && info.hasBaseType()) { - if (isConstructor) { + if (isConstructor || isInterface) { ObjectType infoBaseType = info.getBaseType().evaluate(templateScope, typeRegistry).toMaybeObjectType(); // TODO(sdh): ensure JSDoc's baseType and AST's baseType are compatible if both are set @@ -434,7 +434,7 @@ FunctionTypeBuilder inferInheritance( } else { reportWarning(EXTENDS_WITHOUT_TYPEDEF, formatFnName()); } - } else if (classExtendsType != null && isConstructor) { + } else if (classExtendsType != null && (isConstructor || isInterface)) { // This case is: // // no JSDoc here // class extends astBaseType {...} @@ -937,7 +937,7 @@ FunctionType buildAndRegister() { } private void maybeSetBaseType(FunctionType fnType) { - if (!fnType.isInterface() && baseType != null) { + if (fnType.hasInstanceType() && baseType != null) { fnType.setPrototypeBasedOn(baseType); fnType.extendTemplateTypeMapBasedOn(baseType); } @@ -1050,6 +1050,7 @@ private FunctionType getOrCreateInterface() { } return fnType; } + private void reportWarning(DiagnosticType warning, String ... args) { compiler.report(JSError.make(errorRoot, warning, args)); } diff --git a/src/com/google/javascript/jscomp/TypeCheck.java b/src/com/google/javascript/jscomp/TypeCheck.java index 4281b6f6e81..3c04a1d5bca 100644 --- a/src/com/google/javascript/jscomp/TypeCheck.java +++ b/src/com/google/javascript/jscomp/TypeCheck.java @@ -35,6 +35,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import com.google.common.collect.Streams; import com.google.javascript.jscomp.CodingConvention.SubclassRelationship; import com.google.javascript.jscomp.CodingConvention.SubclassType; import com.google.javascript.jscomp.type.ReverseAbstractInterpreter; @@ -211,6 +212,11 @@ public final class TypeCheck implements NodeTraversal.Callback, CompilerPass { "JSC_HIDDEN_SUPERCLASS_PROPERTY", "property {0} already defined on superclass {1}; use @override to override it"); + static final DiagnosticType HIDDEN_PROTOTYPAL_SUPERTYPE_PROPERTY = + DiagnosticType.disabled( + "JSC_PROTOTYPAL_HIDDEN_SUPERCLASS_PROPERTY", + "property {0} already defined on supertype {1}; use @override to override it"); + // disabled by default. static final DiagnosticType HIDDEN_INTERFACE_PROPERTY = DiagnosticType.disabled( @@ -225,11 +231,24 @@ public final class TypeCheck implements NodeTraversal.Callback, CompilerPass { + "original: {2}\n" + "override: {3}"); + static final DiagnosticType HIDDEN_PROTOTYPAL_SUPERTYPE_PROPERTY_MISMATCH = + DiagnosticType.warning( + "JSC_HIDDEN_PROTOTYPAL_SUPERTYPE_PROPERTY_MISMATCH", + "mismatch of the {0} property type and the type " + + "of the property it overrides from supertype {1}\n" + + "original: {2}\n" + + "override: {3}"); + static final DiagnosticType UNKNOWN_OVERRIDE = DiagnosticType.warning( "JSC_UNKNOWN_OVERRIDE", "property {0} not defined on any superclass of {1}"); + static final DiagnosticType UNKNOWN_PROTOTYPAL_OVERRIDE = + DiagnosticType.warning( + "JSC_UNKNOWN_PROTOTYPAL_OVERRIDE", // + "property {0} not defined on any supertype of {1}"); + static final DiagnosticType INTERFACE_METHOD_OVERRIDE = DiagnosticType.warning( "JSC_INTERFACE_METHOD_OVERRIDE", @@ -316,7 +335,9 @@ public final class TypeCheck implements NodeTraversal.Callback, CompilerPass { CONFLICTING_IMPLEMENTED_TYPE, BAD_IMPLEMENTED_TYPE, HIDDEN_SUPERCLASS_PROPERTY_MISMATCH, + HIDDEN_PROTOTYPAL_SUPERTYPE_PROPERTY_MISMATCH, UNKNOWN_OVERRIDE, + UNKNOWN_PROTOTYPAL_OVERRIDE, INTERFACE_METHOD_OVERRIDE, UNRESOLVED_TYPE, WRONG_ARGUMENT_COUNT, @@ -1236,22 +1257,38 @@ private void checkPropertyInheritanceOnGetpropAssign( // all the methods are implemented. // // As-is, this misses many other ways to override a property. - // - // object.prototype.property = ...; - if (object.isGetProp()) { - Node object2 = object.getFirstChild(); - String property2 = NodeUtil.getStringValue(object.getLastChild()); - - if ("prototype".equals(property2)) { - JSType jsType = getJSType(object2); - if (jsType.isFunctionType()) { - FunctionType functionType = jsType.toMaybeFunctionType(); - if (functionType.isConstructor() || functionType.isInterface()) { - checkDeclaredPropertyInheritance(assign, functionType, property, info, propertyType); - checkAbstractMethodInConcreteClass(assign, functionType, info); - } - } + + if (object.isGetProp() && object.getSecondChild().getString().equals("prototype")) { + // ASSIGN = assign + // GETPROP + // GETPROP = object + // ? = preObject + // STRING = "prototype" + // STRING = property + + Node preObject = object.getFirstChild(); + @Nullable FunctionType ctorType = getJSType(preObject).toMaybeFunctionType(); + if (ctorType == null || !ctorType.hasInstanceType()) { + return; } + + checkDeclaredPropertyAgainstNominalInheritance( + assign, ctorType, property, info, propertyType); + checkAbstractMethodInConcreteClass(assign, ctorType, info); + } else { + // ASSIGN = assign + // GETPROP + // ? = object + // STRING = property + + // We only care about checking a static property assignment. + @Nullable FunctionType ctorType = getJSType(object).toMaybeFunctionType(); + if (ctorType == null || !ctorType.hasInstanceType()) { + return; + } + + checkDeclaredPropertyAgainstPrototypalInheritance( + assign, ctorType, property, info, propertyType); } } @@ -1274,20 +1311,17 @@ private void checkPropertyInheritanceOnPrototypeLitKey( private void checkPropertyInheritanceOnClassMember( Node key, String propertyName, FunctionType ctorType) { if (key.isStaticMember()) { - // TODO(sdh): Handle static members later - will probably need to add an extra boolean - // parameter to checkDeclaredPropertyInheritance, as well as an extra case to getprop - // assigns to check if the owner's type is an ES6 constructor. Put it off for now - // because it's a change in behavior from transpiled code and it may break things. - return; + checkDeclaredPropertyAgainstPrototypalInheritance( + key, ctorType, propertyName, key.getJSDocInfo(), ctorType.getPropertyType(propertyName)); + } else { + checkPropertyInheritance(key, propertyName, ctorType, ctorType.getInstanceType()); } - ObjectType owner = key.isStaticMember() ? ctorType : ctorType.getInstanceType(); - checkPropertyInheritance(key, propertyName, ctorType, owner); } private void checkPropertyInheritance( Node key, String propertyName, FunctionType ctorType, ObjectType type) { if (ctorType != null && (ctorType.isConstructor() || ctorType.isInterface())) { - checkDeclaredPropertyInheritance( + checkDeclaredPropertyAgainstNominalInheritance( key.getFirstChild(), ctorType, propertyName, @@ -1454,14 +1488,27 @@ private static boolean propertyIsImplicitCast(ObjectType type, String prop) { } /** - * Given a constructor type and a property name, check that the property has the JSDoc - * annotation @override iff the property is declared on a superclass. Several checks regarding - * inheritance correctness are also performed. + * Given a {@code ctorType}, check that the property ({@code propertyName}), on the corresponding + * instance type ({@code receiverType}), conforms to inheritance rules. + * + *

This method only checks nominal inheritance (via extends and implements declarations). + * Compare to {@link #checkDeclaredPropertyAgainstPrototypalInheritance()}. + * + *

To be conformant, the {@code propertyName} must + * + *

*/ - private void checkDeclaredPropertyInheritance( - Node n, FunctionType ctorType, String propertyName, JSDocInfo info, JSType propertyType) { - // If the supertype doesn't resolve correctly, we've warned about this - // already. + private void checkDeclaredPropertyAgainstNominalInheritance( + Node n, + FunctionType ctorType, + String propertyName, + @Nullable JSDocInfo info, + JSType propertyType) { + // If the supertype doesn't resolve correctly, we've warned about this already. if (hasUnknownOrEmptySupertype(ctorType)) { return; } @@ -1483,7 +1530,7 @@ private void checkDeclaredPropertyInheritance( superInterfaceHasDeclaredProperty || interfaceType.isPropertyTypeDeclared(propertyName); } } - boolean declaredOverride = info != null && info.isOverride(); + boolean declaredOverride = declaresOverride(info); boolean foundInterfaceProperty = false; if (ctorType.isConstructor()) { @@ -1591,6 +1638,74 @@ private void checkDeclaredPropertyInheritance( } } + /** + * Given a {@code receiverType}, check that the property ({@code propertyName}) conforms to + * inheritance rules. + * + *

This method only checks prototypal inheritance (via the prototype chain). Compare to {@link + * #checkDeclaredPropertyAgainstNominalInheritance()}. + * + *

To be conformant, the {@code propertyName} must + * + *

+ */ + private void checkDeclaredPropertyAgainstPrototypalInheritance( + Node n, + ObjectType receiverType, + String propertyName, + @Nullable JSDocInfo info, + JSType propertyType) { + // TODO(nickreid): Right now this is only expected to run on ctors. However, it wouldn't be bad + // if it ran on more things. Consider a precondition on the arguments. + + boolean declaredOverride = declaresOverride(info); + @Nullable + ObjectType supertypeWithProperty = + Streams.stream(receiverType.getImplicitPrototypeChain()) + // We want to report the supertype that actually had the overidden declaration. + .filter((type) -> type.hasOwnProperty(propertyName)) + // We only care about the lowest match in the chain because it must be the most + // specific. + .findFirst() + .orElse(null); + + if (supertypeWithProperty == null) { + if (declaredOverride) { + compiler.report( + JSError.make( + n, // + UNKNOWN_PROTOTYPAL_OVERRIDE, + propertyName, + receiverType.toString())); + } + } else { + if (!declaredOverride) { + compiler.report( + JSError.make( + n, // + HIDDEN_PROTOTYPAL_SUPERTYPE_PROPERTY, + propertyName, + supertypeWithProperty.toString())); + } + + JSType overriddenPropertyType = supertypeWithProperty.getPropertyType(propertyName); + if (!propertyType.isSubtypeOf(overriddenPropertyType)) { + compiler.report( + JSError.make( + n, + HIDDEN_PROTOTYPAL_SUPERTYPE_PROPERTY_MISMATCH, + propertyName, + supertypeWithProperty.toString(), + overriddenPropertyType.toString(), + propertyType.toString())); + } + } + } + private void checkAbstractMethodInConcreteClass(Node n, FunctionType ctorType, JSDocInfo info) { if (info == null || !info.isAbstract()) { return; @@ -3093,4 +3208,8 @@ private boolean classHasToString(ObjectType type) { } return false; } + + private static boolean declaresOverride(@Nullable JSDocInfo jsdoc) { + return (jsdoc != null) && jsdoc.isOverride(); + } } diff --git a/src/com/google/javascript/rhino/jstype/ObjectType.java b/src/com/google/javascript/rhino/jstype/ObjectType.java index 4b54b4291b5..7ffadd4b3b3 100644 --- a/src/com/google/javascript/rhino/jstype/ObjectType.java +++ b/src/com/google/javascript/rhino/jstype/ObjectType.java @@ -45,6 +45,7 @@ import static com.google.javascript.rhino.jstype.TernaryValue.UNKNOWN; import com.google.common.base.Preconditions; +import com.google.common.collect.AbstractIterator; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -52,6 +53,7 @@ import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.Node; import java.io.Serializable; +import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -319,6 +321,33 @@ public final ObjectType getTopDefiningInterface(String propertyName) { */ public abstract ObjectType getImplicitPrototype(); + /** + * Returns a lazy, dynamic {@link Iterable} for the types forming the implicit prototype chain of + * this type. + * + *

The chain is iterated bottom to top; from the nearest ancestor to the most distant. + * Iteration stops when the next ancestor would be a {@code null} reference. + * + *

The created {@link Iterator}s will not reflect changes to the prototype chain of elements it + * has already iterated past, but will reflect those of upcoming elements. Neither the {@link + * Iterable} nor its {@link Iterator} support mutation. + */ + public final Iterable getImplicitPrototypeChain() { + final ObjectType self = this; + + return () -> + new AbstractIterator() { + + private ObjectType next = self; // We increment past this type before first access. + + @Override + public ObjectType computeNext() { + next = next.getImplicitPrototype(); + return (next != null) ? next : endOfData(); + } + }; + } + /** * Defines a property whose type is explicitly declared by the programmer. * @param propertyName the property's name diff --git a/test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java b/test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java index 06b88b881ec..20bc9f1350c 100644 --- a/test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java +++ b/test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java @@ -3199,7 +3199,7 @@ public void testClassSideInheritanceFillsInParameterTypesWhenCheckingBody() { " static foo(/** string */ arg) {}", "}", "class Bar extends Foo {", - // TODO(sdh): Should need @override here. + " /** @override */", " static foo(arg) {", " var /** null */ x = arg;", " }", @@ -3273,7 +3273,7 @@ public void testClassInheritedMethodReturns() { } @Test - public void testClassStaticMethodParameters() { + public void testStaticMethodParameters() { testTypes( lines( "class C {", @@ -3304,7 +3304,7 @@ public void testClassInheritedStaticMethodParameters() { } @Test - public void testClassStaticMethodReturns() { + public void testStaticMethodReturns() { testTypes( lines( "var D = class {", @@ -3335,7 +3335,7 @@ public void testClassInheritedStaticMethodReturns() { } @Test - public void testClassStaticMethodCalledOnInstance() { + public void testStaticMethodCalledOnInstance() { testTypes( lines( "class C {", @@ -3425,22 +3425,59 @@ public void testClassInstanceMethodOverriddenWithIncompatibleType2() { } @Test - public void testClassStaticMethodOverriddenWithWidenedType() { + public void testStaticMethod_overriddenInBody_withSubtype_atOverride_isOk() { testTypes( lines( "class Base {", " /** @param {string} arg */", " static method(arg) {}", "}", + "", "class Sub extends Base {", - // TODO(sdh): should need @override (new warning: wait until later) - " /** @param {string|number} arg */", + " /**", + " * @override", + // Method is a subtype due to parameter contravariance. + " * @param {string|number} arg", + " */", " static method(arg) {}", "}")); } @Test - public void testClassStaticMethodOverriddenWithIncompatibleType() { + public void testStaticMethod_overriddenInBody_notAtOverride_isBad() { + testTypes( + lines( + "class Base {", + " /** @param {string} arg */", + " static method(arg) {}", + "}", + "", + "class Sub extends Base {", + // Method is a subtype due to parameter contravariance. + " /** @param {string|number} arg */", + " static method(arg) {}", + "}"), + lines( + "property method already defined on supertype function(new:Base): undefined; " + + "use @override to override it")); + } + + @Test + public void testStaticMethod_thatIsNotAnOverride_atOverride_isBad() { + testTypes( + lines( + "class Base {", + " /**", + " * @override", + " * @param {string} arg", + " */", + " static method(arg) {}", + "}"), + lines("property method not defined on any supertype of function(new:Base): undefined")); + } + + @Test + public void testStaticMethod_overriddenInBody_withSupertype_isBad() { testTypes( lines( "class Base {", @@ -3448,19 +3485,22 @@ public void testClassStaticMethodOverriddenWithIncompatibleType() { " static method(arg) {}", "}", "class Sub extends Base {", - " /** @override @param {string} arg */", + " /**", + " * @override", + // Method is a supertype due to parameter contravariance. + " * @param {string} arg", + " */", " static method(arg) {}", - "}")); - // TODO(sdh): This should actually check the override (new warning: wait until later). - // lines( - // "mismatch of the method property type and the type of the property it overrides " - // + "from superclass Base", - // "original: function((number|string)): undefined", - // "override: function(string): undefined")); + "}"), + lines( + "mismatch of the method property type and the type of the property it overrides " + + "from supertype function(new:Base): undefined", + "original: function((number|string)): undefined", + "override: function(string): undefined")); } @Test - public void testClassStaticMethodOverriddenWithIncompatibleInlineType() { + public void testStaticMethod_overriddenInBody_withSupertype_fromInline_isBad() { testTypes( lines( "class Base {", @@ -3469,13 +3509,162 @@ public void testClassStaticMethodOverriddenWithIncompatibleInlineType() { "class Sub extends Base {", " /** @override */", " static method(/** string */ arg) {}", + "}"), + lines( + "mismatch of the method property type and the type of the property it overrides " + + "from supertype function(new:Base): undefined", + "original: function((number|string)): undefined", + "override: function(string): undefined")); + } + + @Test + public void testStaticMethod_overriddenOutsideBody_withSubtype_atOverride_isOk() { + testTypes( + lines( + "class Base {", + " static method(/** string */ arg) {}", + "}", + "", + "class Sub extends Base { }", + "", + "/**", + " * @override", + // Method is a subtype due to parameter contravariance. + " * @param {string|number} arg", + " */", + "Sub.method = function(arg) {};")); + } + + @Test + public void testStaticMethod_overriddenOutsideBody_withSupertype_isBad() { + testTypes( + lines( + "class Base {", + " static method(/** string|number */ arg) {}", + "}", + "", + "class Sub extends Base { }", + "", + "/**", + " * @override", + " * @param {string} arg", + " */", + "Sub.method = function(arg) {};"), + lines( + "mismatch of the method property type and the type of the property it overrides " + + "from supertype function(new:Base): undefined", + "original: function((number|string)): undefined", + "override: function(string): undefined")); + } + + @Test + public void testStaticMethod_onInterface_overriddenInBody_withSubtype_atOverride_isOk() { + testTypes( + lines( + "/** @interface */", + "class Base {", + " /** @param {string} arg */", + " static method(arg) {}", + "}", + "", + "/** @interface */", + "class Sub extends Base {", + " /**", + " * @override", + // Method is a subtype due to parameter contravariance. + " * @param {string|number} arg", + " */", + " static method(arg) {}", "}")); - // TODO(sdh): This should actually check the override. - // lines( - // "mismatch of the method property type and the type of the property it overrides " - // + "from superclass Base", - // "original: function((number|string)): undefined", - // "override: function(string): undefined")); + } + + @Test + public void + testStaticMethod_onNamespacedType_overriddenOutsideBody_withSubtype_atOverride_isOk() { + testTypes( + lines( + "const ns = {};", + "", + "ns.Base = class {", + " /** @param {string} arg */", + " static method(arg) {}", + "};", + "", + "ns.Sub = class extends ns.Base {};", + "", + "/**", + " * @override", + // Method is a subtype due to parameter contravariance. + " * @param {string|number} arg", + " */", + // We specifically want to check that q-name lookups are checked. + "ns.Sub.method = function(arg) {};")); + } + + @Test + public void testStaticMethod_onNamespacedType_overriddenOutsideBody_notAtOverride_isBad() { + testTypes( + lines( + "const ns = {};", + "", + "ns.Base = class {", + " /** @param {string} arg */", + " static method(arg) {}", + "};", + "", + "ns.Sub = class extends ns.Base {};", + "", + // Method is a subtype due to parameter contravariance. + "/** @param {string|number} arg */", + // We specifically want to check that q-name lookups are checked. + "ns.Sub.method = function(arg) {};"), + lines( + "property method already defined on supertype function(new:ns.Base): undefined; " + + "use @override to override it")); + } + + @Test + public void testStaticMethod_onNamespacedType_overridden_withNonSubtype_isBad() { + testTypes( + lines( + "const ns = {};", + "", + "ns.Base = class {", + " /** @param {string} arg */", + " static method(arg) {}", + "};", + "", + "ns.Sub = class extends ns.Base {};", + "", + "/**", + " * @override", + // Method is a subtype due to parameter contravariance. + " * @param {number} arg", + " */", + // We specifically want to check that q-name lookups are checked. + "ns.Sub.method = function(arg) {};"), + lines( + "mismatch of the method property type and the type of the property it overrides " + + "from supertype function(new:ns.Base): undefined", + "original: function(string): undefined", + "override: function(number): undefined")); + } + + @Test + public void testStaticMethod_onNamespacedType_thatIsNotAnOverride_atOverride_isBad() { + testTypes( + lines( + "const ns = {};", + "", + "ns.Base = class {};", + "", + "/**", + " * @override", + " * @param {string} arg", + " */", + // We specifically want to check that q-name lookups are checked. + "ns.Base.method = function(arg) {};"), + lines("property method not defined on any supertype of function(new:ns.Base): undefined")); } @Test