diff --git a/externs/es3.js b/externs/es3.js index 35f08b1a754..52936b501c2 100644 --- a/externs/es3.js +++ b/externs/es3.js @@ -676,8 +676,8 @@ ReadonlyArray.prototype.join = function(opt_separator) {}; /** * Extracts a section of an array and returns a new array. * - * @param {?number=} begin Zero-based index at which to begin extraction. - * @param {?number=} end Zero-based index at which to end extraction. slice + * @param {number=} begin Zero-based index at which to begin extraction. + * @param {number=} end Zero-based index at which to end extraction. slice * extracts up to but not including end. * @return {!Array} * @this {IArrayLike|string} @@ -906,8 +906,8 @@ Array.prototype.shift = function() {}; /** * Extracts a section of an array and returns a new array. * - * @param {?number=} begin Zero-based index at which to begin extraction. - * @param {?number=} end Zero-based index at which to end extraction. slice + * @param {number=} begin Zero-based index at which to begin extraction. + * @param {number=} end Zero-based index at which to end extraction. slice * extracts up to but not including end. * @return {!Array} * @this {IArrayLike|string} @@ -1188,13 +1188,13 @@ Number.prototype.toPrecision = function(opt_precision) {}; /** * Returns a string representing the number. * @this {Number|number} - * @param {(number|Number)=} opt_radix An optional radix. + * @param {number=} radix An optional radix. * @return {string} * @nosideeffects * @see http://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString * @override */ -Number.prototype.toString = function(opt_radix) {}; +Number.prototype.toString = function(radix) {}; // Properties. /** @@ -2178,13 +2178,13 @@ String.prototype.search = function(pattern) {}; /** * @this {String|string} - * @param {number} begin - * @param {number=} opt_end + * @param {number=} begin + * @param {number=} end * @return {string} * @nosideeffects * @see http://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice */ -String.prototype.slice = function(begin, opt_end) {}; +String.prototype.slice = function(begin, end) {}; /** * @this {String|string} diff --git a/externs/es6.js b/externs/es6.js index 364b60e2655..e96cdadb9d6 100644 --- a/externs/es6.js +++ b/externs/es6.js @@ -389,12 +389,12 @@ function ArrayBuffer(length) {} ArrayBuffer.prototype.byteLength; /** - * @param {number} begin - * @param {number=} opt_end + * @param {number=} begin + * @param {number=} end * @return {!ArrayBuffer} * @nosideeffects */ -ArrayBuffer.prototype.slice = function(begin, opt_end) {}; +ArrayBuffer.prototype.slice = function(begin, end) {}; /** * @param {*} arg diff --git a/src/com/google/javascript/jscomp/testing/TestExternsBuilder.java b/src/com/google/javascript/jscomp/testing/TestExternsBuilder.java index beec6e0e088..e9a0badf33f 100644 --- a/src/com/google/javascript/jscomp/testing/TestExternsBuilder.java +++ b/src/com/google/javascript/jscomp/testing/TestExternsBuilder.java @@ -183,6 +183,87 @@ function BigInt(arg) {} BigInt.prototype.valueOf = function() {}; """; + private static final String NUMBER_EXTERNS = + """ + /** + * @constructor + * @param {*=} arg + * @return {number} + */ + function Number(arg) {} + + /** + * @this {Number|number} + * @param {number=} radix + * @return {string} + */ + Number.prototype.toString = function(radix) {}; + + /** + * @return {number} + */ + Number.prototype.valueOf = function() {}; + """; + + private static final String UINT8ARRAY_EXTERNS = + """ + /** + * @constructor + * @implements {IArrayLike} + * @param {*=} arg + * @param {number=} opt_byteOffset + * @param {number=} opt_length + */ + function Uint8Array(arg, opt_byteOffset, opt_length) {} + + /** @type {number} */ + Uint8Array.prototype.length; + + /** + * @param {number=} begin + * @param {number=} end + * @return {!Uint8Array} + * @nosideeffects + */ + Uint8Array.prototype.slice = function(begin, end) {}; + + /** + * @param {number} searchElement + * @param {number=} opt_fromIndex + * @return {number} + * @nosideeffects + */ + Uint8Array.prototype.indexOf = function(searchElement, opt_fromIndex) {}; + + /** + * @param {number} searchElement + * @param {number=} opt_fromIndex + * @return {boolean} + * @nosideeffects + */ + Uint8Array.prototype.includes = function(searchElement, opt_fromIndex) {}; + """; + + private static final String ARRAYBUFFER_EXTERNS = + """ + /** + * @constructor + * @param {number} length + */ + function ArrayBuffer(length) {} + + /** @type {number} */ + ArrayBuffer.prototype.byteLength; + + /** + * @param {number=} begin + * @param {number=} end + * @return {!ArrayBuffer} + * @nosideeffects + */ + ArrayBuffer.prototype.slice = function(begin, end) {}; + """; + private static final String ITERABLE_EXTERNS = """ // Symbol is needed for Symbol.iterator @@ -274,17 +355,22 @@ function String(arg) {} String.prototype[Symbol.iterator] = function() {}; /** @type {number} */ String.prototype.length; - /** @param {number} sliceArg */ - String.prototype.slice = function(sliceArg) {}; /** - * @this {string|!String} + * @this {!String|string} + * @param {number=} begin + * @param {number=} end + * @return {string} + */ + String.prototype.slice = function(begin, end) {}; + /** + * @this {!String|string} * @param {*=} opt_separator * @param {number=} opt_limit * @return {!Array} */ String.prototype.split = function(opt_separator, opt_limit) {}; /** - * @this {string|!String} + * @this {!String|string} * @param {string} search_string * @param {number=} opt_position * @return {boolean} @@ -305,6 +391,12 @@ function String(arg) {} * @return {string} */ String.prototype.charAt = function(index) {}; + /** + * @this {!String|string} + * @param {...*} var_args + * @return {string} + */ + String.prototype.concat = function(var_args) {}; /** * @this {!String|string} * @param {*} regexp @@ -316,7 +408,6 @@ function String(arg) {} * @return {string} */ String.prototype.toLowerCase = function() {}; - /** * @param {number} count * @this {!String|string} @@ -324,7 +415,6 @@ function String(arg) {} * @nosideeffects */ String.prototype.repeat = function(count) {}; - /** * @param {string} searchString * @param {number=} position @@ -332,6 +422,13 @@ function String(arg) {} * @nosideeffects */ String.prototype.includes = function(searchString, position) {}; + /** + * @this {!String|string} + * @param {string|null} searchValue + * @param {(number|null)=} fromIndex + * @return {number} + */ + String.prototype.indexOf = function(searchValue, fromIndex) {}; """; private static final String FUNCTION_EXTERNS = """ @@ -521,8 +618,8 @@ function ReadonlyArray(var_args) {} */ ReadonlyArray.prototype.concat; /** - * @param {?number=} begin Zero-based index at which to begin extraction. - * @param {?number=} end Zero-based index at which to end extraction. slice + * @param {number=} begin Zero-based index at which to begin extraction. + * @param {number=} end Zero-based index at which to end extraction. slice * extracts up to but not including end. * @return {!Array} * @this {!IArrayLike|string} @@ -530,12 +627,18 @@ function ReadonlyArray(var_args) {} * @nosideeffects */ ReadonlyArray.prototype.slice; - + /** + * @param {T} elem + * @param {number=} fromIndex + * @return {number} + * @this {!IArrayLike|string} + * @template T + */ + ReadonlyArray.prototype.indexOf = function(elem, fromIndex) {}; /** * @return {!IteratorIterable} */ ReadonlyArray.prototype.values; - /** * @param {T} searchElement * @param {number=} fromIndex @@ -614,8 +717,8 @@ function Array(var_args) {} Array.prototype.concat = function(var_args) {}; /** * @override - * @param {?number=} begin Zero-based index at which to begin extraction. - * @param {?number=} end Zero-based index at which to end extraction. slice + * @param {number=} begin Zero-based index at which to begin extraction. + * @param {number=} end Zero-based index at which to end extraction. slice * extracts up to but not including end. * @return {!Array} * @this {!IArrayLike|string} @@ -640,6 +743,16 @@ function Array(var_args) {} * @nosideeffects */ Array.prototype.includes = function(searchElement, fromIndex) {}; + + /** + * @param {T} elem + * @param {number=} fromIndex + * @return {number} + * @this {!IArrayLike|string} + * @template T + * @override + */ + Array.prototype.indexOf = function(elem, fromIndex) {}; """; private static final String MAP_EXTERNS = """ @@ -1060,6 +1173,9 @@ function Polymer_LegacyElementMixin(){} """; private boolean includeBigIntExterns = false; + private boolean includeNumberExterns = false; + private boolean includeUint8ArrayExterns = false; + private boolean includeArrayBufferExterns = false; private boolean includeIterableExterns = false; private boolean includeStringExterns = false; private boolean includeFunctionExterns = false; @@ -1091,6 +1207,25 @@ public TestExternsBuilder addBigInt() { return this; } + @CanIgnoreReturnValue + public TestExternsBuilder addNumber() { + includeNumberExterns = true; + return this; + } + + @CanIgnoreReturnValue + public TestExternsBuilder addUint8Array() { + includeUint8ArrayExterns = true; + addArray(); // Uint8Array implements IArrayLike + return this; + } + + @CanIgnoreReturnValue + public TestExternsBuilder addArrayBuffer() { + includeArrayBufferExterns = true; + return this; + } + @CanIgnoreReturnValue public TestExternsBuilder addIterable() { includeIterableExterns = true; @@ -1273,6 +1408,9 @@ public String build() { if (includeBigIntExterns) { externSections.add(BIGINT_EXTERNS); } + if (includeNumberExterns) { + externSections.add(NUMBER_EXTERNS); + } if (includeIterableExterns) { externSections.add(ITERABLE_EXTERNS); } @@ -1288,6 +1426,12 @@ public String build() { if (includeArrayExterns) { externSections.add(ARRAY_EXTERNS); } + if (includeUint8ArrayExterns) { + externSections.add(UINT8ARRAY_EXTERNS); + } + if (includeArrayBufferExterns) { + externSections.add(ARRAYBUFFER_EXTERNS); + } if (includeMapExterns) { externSections.add(MAP_EXTERNS); } diff --git a/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java b/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java index 58287deb180..d662fa5dede 100644 --- a/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java +++ b/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java @@ -764,13 +764,13 @@ public void testReplaceWithCharAt() { foldSame( """ /** @constructor */ function A() {}; - A.prototype.substring = function(begin$jscomp$1, end$jscomp$1) {}; + A.prototype.substring = function(begin$jscomp$2, end$jscomp$2) {}; function f(/** !A */ a) { a.substring(0, 1); } """); foldSame( """ /** @constructor */ function A() {}; - A.prototype.slice = function(begin$jscomp$1, end$jscomp$1) {}; + A.prototype.slice = function(begin$jscomp$2, end$jscomp$2) {}; function f(/** !A */ a) { a.slice(0, 1); } """); diff --git a/test/com/google/javascript/jscomp/SymbolTableTest.java b/test/com/google/javascript/jscomp/SymbolTableTest.java index 7ba07521831..85117f2dd26 100644 --- a/test/com/google/javascript/jscomp/SymbolTableTest.java +++ b/test/com/google/javascript/jscomp/SymbolTableTest.java @@ -1626,12 +1626,15 @@ public void testSymbolForScopeOfNatives() { SymbolTable table = createSymbolTableWithDefaultExterns(""); // From the externs. - Symbol sliceArg = getLocalVar(table, "sliceArg"); - assertThat(sliceArg).isNotNull(); + Symbol slice = getGlobalVar(table, "String.prototype.slice"); + assertThat(slice).isNotNull(); - Symbol scope = table.getSymbolForScope(table.getScope(sliceArg)); + Symbol begin = table.getParameterInFunction(slice, "begin"); + assertThat(begin).isNotNull(); + + Symbol scope = table.getSymbolForScope(table.getScope(begin)); assertThat(scope).isNotNull(); - assertThat(getGlobalVar(table, "String.prototype.slice")).isEqualTo(scope); + assertThat(slice).isEqualTo(scope); Symbol proto = getGlobalVar(table, "String.prototype"); assertThat(proto.getDeclaration().getNode().getSourceFileName()).isEqualTo("externs1"); diff --git a/test/com/google/javascript/jscomp/TypeCheckCovarianceTest.java b/test/com/google/javascript/jscomp/TypeCheckCovarianceTest.java index 1622917911a..94bd7602dfa 100644 --- a/test/com/google/javascript/jscomp/TypeCheckCovarianceTest.java +++ b/test/com/google/javascript/jscomp/TypeCheckCovarianceTest.java @@ -1462,7 +1462,7 @@ function MyArray() {} found : MyArray required: ReadonlyArray<*> missing : [] - mismatch: [includes,takes] + mismatch: [includes,indexOf,takes] """) .addDiagnostic( // this diagnostic is correct - ReadonlyArray.returns() returns a number, while diff --git a/test/com/google/javascript/jscomp/TypeCheckTest.java b/test/com/google/javascript/jscomp/TypeCheckTest.java index ff784c43d83..3d1acf4c907 100644 --- a/test/com/google/javascript/jscomp/TypeCheckTest.java +++ b/test/com/google/javascript/jscomp/TypeCheckTest.java @@ -35,11 +35,13 @@ import com.google.common.collect.ImmutableList; import com.google.javascript.jscomp.deps.ModuleLoader; import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode; +import com.google.javascript.jscomp.parsing.JsDocInfoParser; import com.google.javascript.jscomp.testing.TestExternsBuilder; import com.google.javascript.jscomp.type.ClosureReverseAbstractInterpreter; import com.google.javascript.jscomp.type.SemanticReverseAbstractInterpreter; import com.google.javascript.rhino.IR; import com.google.javascript.rhino.InputId; +import com.google.javascript.rhino.JSTypeExpression; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; import com.google.javascript.rhino.jstype.FunctionType; @@ -77,6 +79,19 @@ function Suggest() {} Cannot add a property to a struct instance after it is constructed. (If you already declared the property, make sure to give it a type.) """; + private static final String UNION_METHOD_CALL_EXTERNS = + new TestExternsBuilder() + .addArray() + .addArrayBuffer() + .addUint8Array() + .addObject() + .addUndefined() + .addFunction() + .addString() + .addBigInt() + .addNumber() + .build(); + @Test public void testInitialTypingScope() { TypedScope s = @@ -23768,11 +23783,75 @@ public void testUnknownTypeReport() { .run(); } + @Test + public void testInferredType_unionReceiverSliceCall() { + assertUnionMethodCallType("!Array|string", ".slice(0, 3)", "!Array|string"); + assertUnionMethodCallType( + "!Array|!ArrayBuffer", ".slice(0, 3)", "!Array|!ArrayBuffer"); + assertUnionMethodCallType( + "!Array|!ArrayBuffer|!Uint8Array", + ".slice(0, 3)", + "!Array|!ArrayBuffer|!Uint8Array"); + } + + @Test + public void testInferredType_unionReceiverIncludesCall() { + assertUnionMethodCallType( + "!Array|string|!ReadonlyArray", ".includes('a')", "boolean"); + assertUnionMethodCallType("!Array|!Uint8Array", ".includes(3)", "boolean"); + } + + @Test + public void testInferredType_unionReceiverIndexOfCall() { + assertUnionMethodCallType("!Array|string", ".indexOf('a')", "number"); + assertUnionMethodCallType("!Array|!Uint8Array", ".indexOf(3)", "number"); + } + + @Test + public void testInferredType_unionReceiverToStringCall() { + assertUnionMethodCallType("number|bigint", ".toString()", "string"); + assertUnionMethodCallType("bigint|number", ".toString(36)", "string"); + } + @Test public void testUnknownTypeReport_allowsUnknownIfStatement() { newTest().addSource("function id(x) { x; }").enableReportUnknownTypes().run(); } + private void assertUnionMethodCallType( + String receiverType, String methodCall, String expectedType) { + Node root = + parseAndTypeCheck( + UNION_METHOD_CALL_EXTERNS, + """ + /** @param {%s} x */ + function callMethodOf(x) { + return x%s; + } + """ + .formatted(receiverType, methodCall)); + + Node functionNode = root.getFirstChild(); + assertNode(functionNode).hasToken(Token.FUNCTION); + + Node returnNode = functionNode.getLastChild().getFirstChild(); + assertNode(returnNode).hasToken(Token.RETURN); + + Node sliceCallNode = returnNode.getFirstChild(); + assertNode(sliceCallNode).hasToken(Token.CALL); + + JSType actualSliceType = sliceCallNode.getJSType(); + assertThat(actualSliceType.isUnknownType()).isFalse(); + assertTypeEquals(parseType(expectedType), actualSliceType); + } + + private JSType parseType(String typeExpression) { + return compiler + .getTypeRegistry() + .evaluateTypeExpressionInGlobalScope( + new JSTypeExpression(JsDocInfoParser.parseTypeString(typeExpression), "")); + } + @Test public void testUnknownForIn() { newTest()