diff --git a/lib/fetch/body.js b/lib/fetch/body.js index 3989b26f620..cf89c7d59f6 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -331,9 +331,7 @@ function bodyMixinMethods (instance) { }, async formData () { - if (!(this instanceof instance)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, instance) throwIfAborted(this[kState]) @@ -435,7 +433,7 @@ function bodyMixinMethods (instance) { throwIfAborted(this[kState]) // Otherwise, throw a TypeError. - webidl.errors.exception({ + throw webidl.errors.exception({ header: `${instance.name}.formData`, message: 'Could not parse content as FormData.' }) @@ -452,11 +450,8 @@ function mixinBody (prototype) { // https://fetch.spec.whatwg.org/#concept-body-consume-body async function specConsumeBody (object, type, instance) { - if (!(object instanceof instance)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(object, instance) - // TODO: why is this needed? throwIfAborted(object[kState]) // 1. If object is unusable, then return a promise rejected diff --git a/lib/fetch/file.js b/lib/fetch/file.js index 2cb100de4be..f7a588c9d0f 100644 --- a/lib/fetch/file.js +++ b/lib/fetch/file.js @@ -76,25 +76,19 @@ class File extends Blob { } get name () { - if (!(this instanceof File)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, File) return this[kState].name } get lastModified () { - if (!(this instanceof File)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, File) return this[kState].lastModified } get type () { - if (!(this instanceof File)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, File) return this[kState].type } @@ -149,65 +143,49 @@ class FileLike { } stream (...args) { - if (!(this instanceof FileLike)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileLike) return this[kState].blobLike.stream(...args) } arrayBuffer (...args) { - if (!(this instanceof FileLike)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileLike) return this[kState].blobLike.arrayBuffer(...args) } slice (...args) { - if (!(this instanceof FileLike)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileLike) return this[kState].blobLike.slice(...args) } text (...args) { - if (!(this instanceof FileLike)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileLike) return this[kState].blobLike.text(...args) } get size () { - if (!(this instanceof FileLike)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileLike) return this[kState].blobLike.size } get type () { - if (!(this instanceof FileLike)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileLike) return this[kState].blobLike.type } get name () { - if (!(this instanceof FileLike)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileLike) return this[kState].name } get lastModified () { - if (!(this instanceof FileLike)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileLike) return this[kState].lastModified } diff --git a/lib/fetch/formdata.js b/lib/fetch/formdata.js index 4b3cc71841a..a2d4ca1a4f5 100644 --- a/lib/fetch/formdata.js +++ b/lib/fetch/formdata.js @@ -10,7 +10,7 @@ const { Blob } = require('buffer') class FormData { constructor (form) { if (form !== undefined) { - webidl.errors.conversionFailed({ + throw webidl.errors.conversionFailed({ prefix: 'FormData constructor', argument: 'Argument 1', types: ['undefined'] @@ -21,9 +21,7 @@ class FormData { } append (name, value, filename = undefined) { - if (!(this instanceof FormData)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FormData) if (arguments.length < 2) { throw new TypeError( @@ -56,9 +54,7 @@ class FormData { } delete (name) { - if (!(this instanceof FormData)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FormData) if (arguments.length < 1) { throw new TypeError( @@ -81,9 +77,7 @@ class FormData { } get (name) { - if (!(this instanceof FormData)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FormData) if (arguments.length < 1) { throw new TypeError( @@ -106,9 +100,7 @@ class FormData { } getAll (name) { - if (!(this instanceof FormData)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FormData) if (arguments.length < 1) { throw new TypeError( @@ -128,9 +120,7 @@ class FormData { } has (name) { - if (!(this instanceof FormData)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FormData) if (arguments.length < 1) { throw new TypeError( @@ -146,9 +136,7 @@ class FormData { } set (name, value, filename = undefined) { - if (!(this instanceof FormData)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FormData) if (arguments.length < 2) { throw new TypeError( @@ -195,9 +183,7 @@ class FormData { } entries () { - if (!(this instanceof FormData)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FormData) return makeIterator( () => this[kState].map(pair => [pair.name, pair.value]), @@ -207,9 +193,7 @@ class FormData { } keys () { - if (!(this instanceof FormData)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FormData) return makeIterator( () => this[kState].map(pair => [pair.name, pair.value]), @@ -219,9 +203,7 @@ class FormData { } values () { - if (!(this instanceof FormData)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FormData) return makeIterator( () => this[kState].map(pair => [pair.name, pair.value]), @@ -235,9 +217,7 @@ class FormData { * @param {unknown} thisArg */ forEach (callbackFn, thisArg = globalThis) { - if (!(this instanceof FormData)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FormData) if (arguments.length < 1) { throw new TypeError( diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 7ba397032d0..962e8e8f7d0 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -38,7 +38,7 @@ function fill (headers, object) { for (const header of object) { // 1. If header does not contain exactly two items, then throw a TypeError. if (header.length !== 2) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'Headers constructor', message: `expected name/value pair to be length 2, found ${header.length}.` }) @@ -56,7 +56,7 @@ function fill (headers, object) { headers.append(key, value) } } else { - webidl.errors.conversionFailed({ + throw webidl.errors.conversionFailed({ prefix: 'Headers constructor', argument: 'Argument 1', types: ['sequence>', 'record'] @@ -173,9 +173,7 @@ class Headers { // https://fetch.spec.whatwg.org/#dom-headers-append append (name, value) { - if (!(this instanceof Headers)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Headers) if (arguments.length < 2) { throw new TypeError( @@ -192,13 +190,13 @@ class Headers { // 2. If name is not a header name or value is not a // header value, then throw a TypeError. if (!isValidHeaderName(name)) { - webidl.errors.invalidArgument({ + throw webidl.errors.invalidArgument({ prefix: 'Headers.append', value: name, type: 'header name' }) } else if (!isValidHeaderValue(value)) { - webidl.errors.invalidArgument({ + throw webidl.errors.invalidArgument({ prefix: 'Headers.append', value, type: 'header value' @@ -227,9 +225,7 @@ class Headers { // https://fetch.spec.whatwg.org/#dom-headers-delete delete (name) { - if (!(this instanceof Headers)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Headers) if (arguments.length < 1) { throw new TypeError( @@ -241,7 +237,7 @@ class Headers { // 1. If name is not a header name, then throw a TypeError. if (!isValidHeaderName(name)) { - webidl.errors.invalidArgument({ + throw webidl.errors.invalidArgument({ prefix: 'Headers.delete', value: name, type: 'header name' @@ -278,9 +274,7 @@ class Headers { // https://fetch.spec.whatwg.org/#dom-headers-get get (name) { - if (!(this instanceof Headers)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Headers) if (arguments.length < 1) { throw new TypeError( @@ -292,7 +286,7 @@ class Headers { // 1. If name is not a header name, then throw a TypeError. if (!isValidHeaderName(name)) { - webidl.errors.invalidArgument({ + throw webidl.errors.invalidArgument({ prefix: 'Headers.get', value: name, type: 'header name' @@ -306,9 +300,7 @@ class Headers { // https://fetch.spec.whatwg.org/#dom-headers-has has (name) { - if (!(this instanceof Headers)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Headers) if (arguments.length < 1) { throw new TypeError( @@ -320,7 +312,7 @@ class Headers { // 1. If name is not a header name, then throw a TypeError. if (!isValidHeaderName(name)) { - webidl.errors.invalidArgument({ + throw webidl.errors.invalidArgument({ prefix: 'Headers.has', value: name, type: 'header name' @@ -334,9 +326,7 @@ class Headers { // https://fetch.spec.whatwg.org/#dom-headers-set set (name, value) { - if (!(this instanceof Headers)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Headers) if (arguments.length < 2) { throw new TypeError( @@ -353,13 +343,13 @@ class Headers { // 2. If name is not a header name or value is not a // header value, then throw a TypeError. if (!isValidHeaderName(name)) { - webidl.errors.invalidArgument({ + throw webidl.errors.invalidArgument({ prefix: 'Headers.set', value: name, type: 'header name' }) } else if (!isValidHeaderValue(value)) { - webidl.errors.invalidArgument({ + throw webidl.errors.invalidArgument({ prefix: 'Headers.set', value, type: 'header value' @@ -395,9 +385,7 @@ class Headers { } keys () { - if (!(this instanceof Headers)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Headers) return makeIterator( () => [...this[kHeadersSortedMap].entries()], @@ -407,9 +395,7 @@ class Headers { } values () { - if (!(this instanceof Headers)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Headers) return makeIterator( () => [...this[kHeadersSortedMap].entries()], @@ -419,9 +405,7 @@ class Headers { } entries () { - if (!(this instanceof Headers)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Headers) return makeIterator( () => [...this[kHeadersSortedMap].entries()], @@ -435,9 +419,7 @@ class Headers { * @param {unknown} thisArg */ forEach (callbackFn, thisArg = globalThis) { - if (!(this instanceof Headers)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Headers) if (arguments.length < 1) { throw new TypeError( @@ -457,9 +439,7 @@ class Headers { } [Symbol.for('nodejs.util.inspect.custom')] () { - if (!(this instanceof Headers)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Headers) return this[kHeadersList] } @@ -493,7 +473,7 @@ webidl.converters.HeadersInit = function (V) { return webidl.converters['record'](V) } - webidl.errors.conversionFailed({ + throw webidl.errors.conversionFailed({ prefix: 'Headers constructor', argument: 'Argument 1', types: ['sequence>', 'record'] diff --git a/lib/fetch/request.js b/lib/fetch/request.js index bd4453eba65..40e54d27ab6 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -256,7 +256,7 @@ class Request { // 17. If mode is "navigate", then throw a TypeError. if (mode === 'navigate') { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'Request constructor', message: 'invalid request mode navigate.' }) @@ -500,9 +500,7 @@ class Request { // Returns request’s HTTP method, which is "GET" by default. get method () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The method getter steps are to return this’s request’s method. return this[kState].method @@ -510,9 +508,7 @@ class Request { // Returns the URL of request as a string. get url () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The url getter steps are to return this’s request’s URL, serialized. return URLSerializer(this[kState].url) @@ -522,9 +518,7 @@ class Request { // Note that headers added in the network layer by the user agent will not // be accounted for in this object, e.g., the "Host" header. get headers () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The headers getter steps are to return this’s headers. return this[kHeaders] @@ -533,9 +527,7 @@ class Request { // Returns the kind of resource requested by request, e.g., "document" // or "script". get destination () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The destination getter are to return this’s request’s destination. return this[kState].destination @@ -547,9 +539,7 @@ class Request { // during fetching to determine the value of the `Referer` header of the // request being made. get referrer () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // 1. If this’s request’s referrer is "no-referrer", then return the // empty string. @@ -571,9 +561,7 @@ class Request { // This is used during fetching to compute the value of the request’s // referrer. get referrerPolicy () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The referrerPolicy getter steps are to return this’s request’s referrer policy. return this[kState].referrerPolicy @@ -583,9 +571,7 @@ class Request { // whether the request will use CORS, or will be restricted to same-origin // URLs. get mode () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The mode getter steps are to return this’s request’s mode. return this[kState].mode @@ -603,9 +589,7 @@ class Request { // which is a string indicating how the request will // interact with the browser’s cache when fetching. get cache () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The cache getter steps are to return this’s request’s cache mode. return this[kState].cache @@ -616,9 +600,7 @@ class Request { // request will be handled during fetching. A request // will follow redirects by default. get redirect () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The redirect getter steps are to return this’s request’s redirect mode. return this[kState].redirect @@ -628,9 +610,7 @@ class Request { // cryptographic hash of the resource being fetched. Its value // consists of multiple hashes separated by whitespace. [SRI] get integrity () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The integrity getter steps are to return this’s request’s integrity // metadata. @@ -640,9 +620,7 @@ class Request { // Returns a boolean indicating whether or not request can outlive the // global in which it was created. get keepalive () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The keepalive getter steps are to return this’s request’s keepalive. return this[kState].keepalive @@ -651,9 +629,7 @@ class Request { // Returns a boolean indicating whether or not request is for a reload // navigation. get isReloadNavigation () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The isReloadNavigation getter steps are to return true if this’s // request’s reload-navigation flag is set; otherwise false. @@ -663,9 +639,7 @@ class Request { // Returns a boolean indicating whether or not request is for a history // navigation (a.k.a. back-foward navigation). get isHistoryNavigation () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The isHistoryNavigation getter steps are to return true if this’s request’s // history-navigation flag is set; otherwise false. @@ -676,43 +650,33 @@ class Request { // object indicating whether or not request has been aborted, and its // abort event handler. get signal () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // The signal getter steps are to return this’s signal. return this[kSignal] } get body () { - if (!this || !this[kState]) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) return this[kState].body ? this[kState].body.stream : null } get bodyUsed () { - if (!this || !this[kState]) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) return !!this[kState].body && util.isDisturbed(this[kState].body.stream) } get duplex () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) return 'half' } // Returns a clone of request. clone () { - if (!(this instanceof Request)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Request) // 1. If this is unusable, then throw a TypeError. if (this.bodyUsed || this.body?.locked) { diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 6ef9aa1c423..d3151d88d40 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -171,9 +171,7 @@ class Response { // Returns response’s type, e.g., "cors". get type () { - if (!(this instanceof Response)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Response) // The type getter steps are to return this’s response’s type. return this[kState].type @@ -181,9 +179,7 @@ class Response { // Returns response’s URL, if it has one; otherwise the empty string. get url () { - if (!(this instanceof Response)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Response) const urlList = this[kState].urlList @@ -201,9 +197,7 @@ class Response { // Returns whether response was obtained through a redirect. get redirected () { - if (!(this instanceof Response)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Response) // The redirected getter steps are to return true if this’s response’s URL // list has more than one item; otherwise false. @@ -212,9 +206,7 @@ class Response { // Returns response’s status. get status () { - if (!(this instanceof Response)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Response) // The status getter steps are to return this’s response’s status. return this[kState].status @@ -222,9 +214,7 @@ class Response { // Returns whether response’s status is an ok status. get ok () { - if (!(this instanceof Response)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Response) // The ok getter steps are to return true if this’s response’s status is an // ok status; otherwise false. @@ -233,9 +223,7 @@ class Response { // Returns response’s status message. get statusText () { - if (!(this instanceof Response)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Response) // The statusText getter steps are to return this’s response’s status // message. @@ -244,39 +232,31 @@ class Response { // Returns response’s headers as Headers. get headers () { - if (!(this instanceof Response)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Response) // The headers getter steps are to return this’s headers. return this[kHeaders] } get body () { - if (!this || !this[kState]) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Response) return this[kState].body ? this[kState].body.stream : null } get bodyUsed () { - if (!this || !this[kState]) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Response) return !!this[kState].body && util.isDisturbed(this[kState].body.stream) } // Returns a clone of response. clone () { - if (!(this instanceof Response)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, Response) // 1. If this is unusable, then throw a TypeError. if (this.bodyUsed || (this.body && this.body.locked)) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'Response.clone', message: 'Body has already been consumed.' }) @@ -504,7 +484,7 @@ function initializeResponse (response, init, body) { if (body) { // 1. If response's status is a null body status, then throw a TypeError. if (nullBodyStatus.includes(response.status)) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'Response constructor', message: 'Invalid response status code.' }) diff --git a/lib/fetch/webidl.js b/lib/fetch/webidl.js index cd42de2ac8a..31dcb1ddfa6 100644 --- a/lib/fetch/webidl.js +++ b/lib/fetch/webidl.js @@ -3,30 +3,16 @@ const { types } = require('util') const { hasOwn, toUSVString } = require('./util') +/** @type {import('../../types/webidl').Webidl} */ const webidl = {} webidl.converters = {} webidl.util = {} webidl.errors = {} -/** - * - * @param {{ - * header: string - * message: string - * }} message - */ webidl.errors.exception = function (message) { - throw new TypeError(`${message.header}: ${message.message}`) + return new TypeError(`${message.header}: ${message.message}`) } -/** - * Throw an error when conversion from one type to another has failed - * @param {{ - * prefix: string - * argument: string - * types: string[] - * }} context - */ webidl.errors.conversionFailed = function (context) { const plural = context.types.length === 1 ? '' : ' one of' const message = @@ -39,14 +25,6 @@ webidl.errors.conversionFailed = function (context) { }) } -/** - * Throw an error when an invalid argument is provided - * @param {{ - * prefix: string - * value: string - * type: string - * }} context - */ webidl.errors.invalidArgument = function (context) { return webidl.errors.exception({ header: context.prefix, @@ -54,6 +32,13 @@ webidl.errors.invalidArgument = function (context) { }) } +// https://webidl.spec.whatwg.org/#implements +webidl.brandCheck = function (V, I) { + if (!(V instanceof I)) { + throw new TypeError('Illegal invocation') + } +} + // https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values webidl.util.Type = function (V) { switch (typeof V) { @@ -126,7 +111,7 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) { x === Number.POSITIVE_INFINITY || x === Number.NEGATIVE_INFINITY ) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'Integer conversion', message: `Could not convert ${V} to an integer.` }) @@ -138,7 +123,7 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) { // 3. If x < lowerBound or x > upperBound, then // throw a TypeError. if (x < lowerBound || x > upperBound) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'Integer conversion', message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.` }) @@ -213,7 +198,7 @@ webidl.sequenceConverter = function (converter) { return (V) => { // 1. If Type(V) is not Object, throw a TypeError. if (webidl.util.Type(V) !== 'Object') { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'Sequence', message: `Value of type ${webidl.util.Type(V)} is not an Object.` }) @@ -229,7 +214,7 @@ webidl.sequenceConverter = function (converter) { method === undefined || typeof method.next !== 'function' ) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'Sequence', message: 'Object is not an iterator.' }) @@ -255,7 +240,7 @@ webidl.recordConverter = function (keyConverter, valueConverter) { return (O) => { // 1. If Type(O) is not Object, throw a TypeError. if (webidl.util.Type(O) !== 'Object') { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'Record', message: `Value of type ${webidl.util.Type(O)} is not an Object.` }) @@ -314,7 +299,7 @@ webidl.recordConverter = function (keyConverter, valueConverter) { webidl.interfaceConverter = function (i) { return (V, opts = {}) => { if (opts.strict !== false && !(V instanceof i)) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: i.name, message: `Expected ${V} to be an instance of ${i.name}.` }) @@ -324,16 +309,6 @@ webidl.interfaceConverter = function (i) { } } -/** - * @param {{ - * key: string, - * defaultValue?: any, - * required?: boolean, - * converter: (...args: unknown[]) => unknown, - * allowedValues?: any[] - * }[]} converters - * @returns - */ webidl.dictionaryConverter = function (converters) { return (dictionary) => { const type = webidl.util.Type(dictionary) @@ -342,7 +317,7 @@ webidl.dictionaryConverter = function (converters) { if (type === 'Null' || type === 'Undefined') { return dict } else if (type !== 'Object') { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'Dictionary', message: `Expected ${dictionary} to be one of: Null, Undefined, Object.` }) @@ -353,7 +328,7 @@ webidl.dictionaryConverter = function (converters) { if (required === true) { if (!hasOwn(dictionary, key)) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'Dictionary', message: `Missing required key "${key}".` }) @@ -379,7 +354,7 @@ webidl.dictionaryConverter = function (converters) { options.allowedValues && !options.allowedValues.includes(value) ) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'Dictionary', message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.` }) @@ -450,7 +425,6 @@ webidl.converters.ByteString = function (V) { } // https://webidl.spec.whatwg.org/#es-USVString -// TODO: ensure that util.toUSVString follows webidl spec webidl.converters.USVString = toUSVString // https://webidl.spec.whatwg.org/#es-boolean @@ -469,9 +443,9 @@ webidl.converters.any = function (V) { } // https://webidl.spec.whatwg.org/#es-long-long -webidl.converters['long long'] = function (V, opts) { +webidl.converters['long long'] = function (V) { // 1. Let x be ? ConvertToInt(V, 64, "signed"). - const x = webidl.util.ConvertToInt(V, 64, 'signed', opts) + const x = webidl.util.ConvertToInt(V, 64, 'signed') // 2. Return the IDL long long value that represents // the same numeric value as x. @@ -509,7 +483,7 @@ webidl.converters.ArrayBuffer = function (V, opts = {}) { webidl.util.Type(V) !== 'Object' || !types.isAnyArrayBuffer(V) ) { - webidl.errors.conversionFailed({ + throw webidl.errors.conversionFailed({ prefix: `${V}`, argument: `${V}`, types: ['ArrayBuffer'] @@ -521,7 +495,7 @@ webidl.converters.ArrayBuffer = function (V, opts = {}) { // IsSharedArrayBuffer(V) is true, then throw a // TypeError. if (opts.allowShared === false && types.isSharedArrayBuffer(V)) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'ArrayBuffer', message: 'SharedArrayBuffer is not allowed.' }) @@ -549,7 +523,7 @@ webidl.converters.TypedArray = function (V, T, opts = {}) { !types.isTypedArray(V) || V.constructor.name !== T.name ) { - webidl.errors.conversionFailed({ + throw webidl.errors.conversionFailed({ prefix: `${T.name}`, argument: `${V}`, types: [T.name] @@ -561,7 +535,7 @@ webidl.converters.TypedArray = function (V, T, opts = {}) { // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is // true, then throw a TypeError. if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'ArrayBuffer', message: 'SharedArrayBuffer is not allowed.' }) @@ -582,7 +556,7 @@ webidl.converters.DataView = function (V, opts = {}) { // 1. If Type(V) is not Object, or V does not have a // [[DataView]] internal slot, then throw a TypeError. if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'DataView', message: 'Object is not a DataView.' }) @@ -593,7 +567,7 @@ webidl.converters.DataView = function (V, opts = {}) { // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true, // then throw a TypeError. if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { - webidl.errors.exception({ + throw webidl.errors.exception({ header: 'ArrayBuffer', message: 'SharedArrayBuffer is not allowed.' }) diff --git a/lib/fileapi/filereader.js b/lib/fileapi/filereader.js index fa18e8eb593..2bc2c46ac6a 100644 --- a/lib/fileapi/filereader.js +++ b/lib/fileapi/filereader.js @@ -37,9 +37,7 @@ class FileReader extends EventTarget { * @param {import('buffer').Blob} blob */ readAsArrayBuffer (blob) { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) if (arguments.length === 0) { throw new TypeError( @@ -59,9 +57,7 @@ class FileReader extends EventTarget { * @param {import('buffer').Blob} blob */ readAsBinaryString (blob) { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) if (arguments.length === 0) { throw new TypeError( @@ -82,9 +78,7 @@ class FileReader extends EventTarget { * @param {string?} encoding */ readAsText (blob, encoding = undefined) { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) if (arguments.length === 0) { throw new TypeError( @@ -108,9 +102,7 @@ class FileReader extends EventTarget { * @param {import('buffer').Blob} blob */ readAsDataURL (blob) { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) if (arguments.length === 0) { throw new TypeError( @@ -166,9 +158,7 @@ class FileReader extends EventTarget { * @see https://w3c.github.io/FileAPI/#dom-filereader-readystate */ get readyState () { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) switch (this[kState]) { case 'empty': return this.EMPTY @@ -181,9 +171,7 @@ class FileReader extends EventTarget { * @see https://w3c.github.io/FileAPI/#dom-filereader-result */ get result () { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) // The result attribute’s getter, when invoked, must return // this's result. @@ -194,9 +182,7 @@ class FileReader extends EventTarget { * @see https://w3c.github.io/FileAPI/#dom-filereader-error */ get error () { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) // The error attribute’s getter, when invoked, must return // this's error. @@ -204,17 +190,13 @@ class FileReader extends EventTarget { } get onloadend () { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) return this[kEvents].loadend } set onloadend (fn) { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) if (typeof fn === 'function') { this[kEvents].loadend = fn @@ -224,17 +206,13 @@ class FileReader extends EventTarget { } get onerror () { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) return this[kEvents].error } set onerror (fn) { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) if (typeof fn === 'function') { this[kEvents].error = fn @@ -244,17 +222,13 @@ class FileReader extends EventTarget { } get onloadstart () { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) return this[kEvents].loadstart } set onloadstart (fn) { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) if (typeof fn === 'function') { this[kEvents].loadstart = fn @@ -264,17 +238,13 @@ class FileReader extends EventTarget { } get onprogress () { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) return this[kEvents].progress } set onprogress (fn) { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) if (typeof fn === 'function') { this[kEvents].progress = fn @@ -284,17 +254,13 @@ class FileReader extends EventTarget { } get onload () { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) return this[kEvents].load } set onload (fn) { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) if (typeof fn === 'function') { this[kEvents].load = fn @@ -304,17 +270,13 @@ class FileReader extends EventTarget { } get onabort () { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) return this[kEvents].abort } set onabort (fn) { - if (!(this instanceof FileReader)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, FileReader) if (typeof fn === 'function') { this[kEvents].abort = fn diff --git a/lib/fileapi/progressevent.js b/lib/fileapi/progressevent.js index ab3c6191e2d..778cf224c6a 100644 --- a/lib/fileapi/progressevent.js +++ b/lib/fileapi/progressevent.js @@ -22,25 +22,19 @@ class ProgressEvent extends Event { } get lengthComputable () { - if (!(this instanceof ProgressEvent)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, ProgressEvent) return this[kState].lengthComputable } get loaded () { - if (!(this instanceof ProgressEvent)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, ProgressEvent) return this[kState].loaded } get total () { - if (!(this instanceof ProgressEvent)) { - throw new TypeError('Illegal invocation') - } + webidl.brandCheck(this, ProgressEvent) return this[kState].total } diff --git a/types/webidl.d.ts b/types/webidl.d.ts new file mode 100644 index 00000000000..c284c44ea3e --- /dev/null +++ b/types/webidl.d.ts @@ -0,0 +1,208 @@ +// These types are not exported, and are only used internally + +/** + * Take in an unknown value and return one that is of type T + */ +type Converter = (object: unknown) => T + +type SequenceConverter = (object: unknown) => T[] + +type RecordConverter = (object: unknown) => Record + +interface ConvertToIntOpts { + clamp?: boolean + enforceRange?: boolean +} + +interface WebidlErrors { + exception (opts: { header: string, message: string }): TypeError + /** + * @description Throw an error when conversion from one type to another has failed + */ + conversionFailed (opts: { + prefix: string + argument: string + types: string[] + }): TypeError + /** + * @description Throw an error when an invalid argument is provided + */ + invalidArgument (opts: { + prefix: string + value: string + type: string + }): TypeError +} + +interface WebidlUtil { + /** + * @see https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values + */ + Type (object: unknown): + | 'Undefined' + | 'Boolean' + | 'String' + | 'Symbol' + | 'Number' + | 'BigInt' + | 'Null' + | 'Object' + + /** + * @see https://webidl.spec.whatwg.org/#abstract-opdef-converttoint + */ + ConvertToInt ( + V: unknown, + bitLength: number, + signedness: 'signed' | 'unsigned', + opts?: ConvertToIntOpts + ): number + + /** + * @see https://webidl.spec.whatwg.org/#abstract-opdef-converttoint + */ + IntegerPart (N: number): number +} + +interface WebidlConverters { + /** + * @see https://webidl.spec.whatwg.org/#es-DOMString + */ + DOMString (V: unknown, opts?: { + legacyNullToEmptyString: boolean + }): string + + /** + * @see https://webidl.spec.whatwg.org/#es-ByteString + */ + ByteString (V: unknown): string + + /** + * @see https://webidl.spec.whatwg.org/#es-USVString + */ + USVString (V: unknown): string + + /** + * @see https://webidl.spec.whatwg.org/#es-boolean + */ + boolean (V: unknown): boolean + + /** + * @see https://webidl.spec.whatwg.org/#es-any + */ + any (V: Value): Value + + /** + * @see https://webidl.spec.whatwg.org/#es-long-long + */ + ['long long'] (V: unknown): number + + /** + * @see https://webidl.spec.whatwg.org/#es-unsigned-long-long + */ + ['unsigned long long'] (V: unknown): number + + /** + * @see https://webidl.spec.whatwg.org/#es-unsigned-short + */ + ['unsigned short'] (V: unknown): number + + /** + * @see https://webidl.spec.whatwg.org/#idl-ArrayBuffer + */ + ArrayBuffer (V: unknown): ArrayBufferLike + ArrayBuffer (V: unknown, opts: { allowShared: false }): ArrayBuffer + + /** + * @see https://webidl.spec.whatwg.org/#es-buffer-source-types + */ + TypedArray ( + V: unknown, + TypedArray: NodeJS.TypedArray | ArrayBufferLike + ): NodeJS.TypedArray | ArrayBufferLike + TypedArray ( + V: unknown, + TypedArray: NodeJS.TypedArray | ArrayBufferLike, + opts?: { allowShared: false } + ): NodeJS.TypedArray | ArrayBuffer + + /** + * @see https://webidl.spec.whatwg.org/#es-buffer-source-types + */ + DataView (V: unknown, opts?: { allowShared: boolean }): DataView + + /** + * @see https://webidl.spec.whatwg.org/#BufferSource + */ + BufferSource ( + V: unknown, + opts?: { allowShared: boolean } + ): NodeJS.TypedArray | ArrayBufferLike | DataView + + ['sequence']: SequenceConverter + + ['sequence>']: SequenceConverter + + ['record']: RecordConverter + + [Key: string]: (...args: any[]) => unknown +} + +export interface Webidl { + errors: WebidlErrors + util: WebidlUtil + converters: WebidlConverters + + /** + * @description Performs a brand-check on {@param V} to ensure it is a + * {@param cls} object. + */ + brandCheck (V: unknown, cls: Interface): asserts V is Interface + + /** + * @see https://webidl.spec.whatwg.org/#es-sequence + * @description Convert a value, V, to a WebIDL sequence type. + */ + sequenceConverter (C: Converter): SequenceConverter + + /** + * @see https://webidl.spec.whatwg.org/#es-to-record + * @description Convert a value, V, to a WebIDL record type. + */ + recordConverter ( + keyConverter: Converter, + valueConverter: Converter + ): RecordConverter + + /** + * Similar to {@link Webidl.brandCheck} but allows skipping the check if third party + * interfaces are allowed. + */ + interfaceConverter (cls: Interface): ( + V: unknown, + opts?: { strict: boolean } + ) => asserts V is typeof cls + + // TODO(@KhafraDev): a type could likely be implemented that can infer the return type + // from the converters given? + /** + * Converts a value, V, to a WebIDL dictionary types. Allows limiting which keys are + * allowed, values allowed, optional and required keys. Auto converts the value to + * a type given a converter. + */ + dictionaryConverter (converters: { + key: string, + defaultValue?: unknown, + required?: boolean, + converter: (...args: unknown[]) => unknown, + allowedValues?: unknown[] + }[]): (V: unknown) => Record + + /** + * @see https://webidl.spec.whatwg.org/#idl-nullable-type + * @description allows a type, V, to be null + */ + nullableConverter ( + converter: Converter + ): (V: unknown) => ReturnType | null +}