diff --git a/.eslintrc.js b/.eslintrc.js index c971aa16e35..d2aaef6b9ed 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -71,7 +71,8 @@ module.exports = { }; return acc; }, {})), - extends: "extends" + extends: "extends", + constructor: "constructor" } } }, diff --git a/declarations/WebpackOptions.d.ts b/declarations/WebpackOptions.d.ts index bba927cd26e..617bf45136d 100644 --- a/declarations/WebpackOptions.d.ts +++ b/declarations/WebpackOptions.d.ts @@ -1094,7 +1094,7 @@ export interface OutputOptions { /** * Algorithm used for generation the hash (see node.js crypto package) */ - hashFunction?: string | (new () => import("../lib/util/createHash").Hash); + hashFunction?: string | import("../lib/util/createHash").HashConstructor; /** * Any string which is added to the hash to salt it */ diff --git a/lib/AbstractMethodError.js b/lib/AbstractMethodError.js new file mode 100644 index 00000000000..c9eb9cffb5a --- /dev/null +++ b/lib/AbstractMethodError.js @@ -0,0 +1,43 @@ +"use strict"; + +const WebpackError = require("./WebpackError"); +const CURRENT_METHOD_REGEXP = /at ([a-zA-Z0-9_.]*)/; + +/** + * @param {string=} method method name + * @returns {string} message + */ +function createMessage(method) { + return `Abstract method${method ? " " + method : ""}. Must be overridden.`; +} + +/** + * @constructor + */ +function Message() { + this.stack = undefined; + Error.captureStackTrace(this); + /** @type {RegExpMatchArray} */ + const match = this.stack.split("\n")[3].match(CURRENT_METHOD_REGEXP); + + this.message = match && match[1] ? createMessage(match[1]) : createMessage(); +} + +/** + * Error for abstract method + * @example + * class FooClass { + * abstractMethod() { + * throw new AbstractMethodError(); // error message: Abstract method FooClass.abstractMethod. Must be overriden. + * } + * } + * + */ +class AbstractMethodError extends WebpackError { + constructor() { + super(new Message().message); + this.name = "AbstractMethodError"; + } +} + +module.exports = AbstractMethodError; diff --git a/lib/Compilation.js b/lib/Compilation.js index 80ef083e38f..a56b0a9c4e2 100644 --- a/lib/Compilation.js +++ b/lib/Compilation.js @@ -2294,7 +2294,7 @@ class Compilation extends Tapable { const module = modules[i]; const moduleHash = createHash(hashFunction); module.updateHash(moduleHash); - module.hash = moduleHash.digest(hashDigest); + module.hash = /** @type {string} */ (moduleHash.digest(hashDigest)); module.renderedHash = module.hash.substr(0, hashDigestLength); } // clone needed as sort below is inplace mutation @@ -2329,7 +2329,7 @@ class Compilation extends Tapable { this.dependencyTemplates ); this.hooks.chunkHash.call(chunk, chunkHash); - chunk.hash = chunkHash.digest(hashDigest); + chunk.hash = /** @type {string} */ (chunkHash.digest(hashDigest)); hash.update(chunk.hash); chunk.renderedHash = chunk.hash.substr(0, hashDigestLength); this.hooks.contentHash.call(chunk); @@ -2337,7 +2337,7 @@ class Compilation extends Tapable { this.errors.push(new ChunkRenderError(chunk, "", err)); } } - this.fullHash = hash.digest(hashDigest); + this.fullHash = /** @type {string} */ (hash.digest(hashDigest)); this.hash = this.fullHash.substr(0, hashDigestLength); } @@ -2353,7 +2353,7 @@ class Compilation extends Tapable { const hash = createHash(hashFunction); hash.update(this.fullHash); hash.update(update); - this.fullHash = hash.digest(hashDigest); + this.fullHash = /** @type {string} */ (hash.digest(hashDigest)); this.hash = this.fullHash.substr(0, hashDigestLength); } diff --git a/lib/HashedModuleIdsPlugin.js b/lib/HashedModuleIdsPlugin.js index 0c720c181b2..7a860f74f44 100644 --- a/lib/HashedModuleIdsPlugin.js +++ b/lib/HashedModuleIdsPlugin.js @@ -45,7 +45,9 @@ class HashedModuleIdsPlugin { }); const hash = createHash(options.hashFunction); hash.update(id); - const hashId = hash.digest(options.hashDigest); + const hashId = /** @type {string} */ (hash.digest( + options.hashDigest + )); let len = options.hashDigestLength; while (usedIds.has(hashId.substr(0, len))) len++; module.id = hashId.substr(0, len); diff --git a/lib/JavascriptModulesPlugin.js b/lib/JavascriptModulesPlugin.js index 07030c9a301..2c1bbe4d45d 100644 --- a/lib/JavascriptModulesPlugin.js +++ b/lib/JavascriptModulesPlugin.js @@ -148,9 +148,8 @@ class JavascriptModulesPlugin { hash.update(m.hash); } } - chunk.contentHash.javascript = hash - .digest(hashDigest) - .substr(0, hashDigestLength); + const digest = /** @type {string} */ (hash.digest(hashDigest)); + chunk.contentHash.javascript = digest.substr(0, hashDigestLength); }); } ); diff --git a/lib/ModuleFilenameHelpers.js b/lib/ModuleFilenameHelpers.js index 105e89e3fa8..bd0742b7f70 100644 --- a/lib/ModuleFilenameHelpers.js +++ b/lib/ModuleFilenameHelpers.js @@ -44,7 +44,8 @@ const getBefore = (str, token) => { const getHash = str => { const hash = createHash("md4"); hash.update(str); - return hash.digest("hex").substr(0, 4); + const digest = /** @type {string} */ (hash.digest("hex")); + return digest.substr(0, 4); }; const asRegExp = test => { diff --git a/lib/NamedModulesPlugin.js b/lib/NamedModulesPlugin.js index a3857ac8e90..2d84aafe297 100644 --- a/lib/NamedModulesPlugin.js +++ b/lib/NamedModulesPlugin.js @@ -10,7 +10,8 @@ const RequestShortener = require("./RequestShortener"); const getHash = str => { const hash = createHash("md4"); hash.update(str); - return hash.digest("hex").substr(0, 4); + const digest = /** @type {string} */ (hash.digest("hex")); + return digest.substr(0, 4); }; class NamedModulesPlugin { diff --git a/lib/NormalModule.js b/lib/NormalModule.js index ec54e07c843..ba8d31819f2 100644 --- a/lib/NormalModule.js +++ b/lib/NormalModule.js @@ -405,7 +405,7 @@ class NormalModule extends Module { } hash.update("meta"); hash.update(JSON.stringify(this.buildMeta)); - this._buildHash = hash.digest("hex"); + this._buildHash = /** @type {string} */ (hash.digest("hex")); } build(options, compilation, resolver, fs, callback) { diff --git a/lib/util/createHash.js b/lib/util/createHash.js index fa18ffd8d0d..64de510da5c 100644 --- a/lib/util/createHash.js +++ b/lib/util/createHash.js @@ -4,21 +4,50 @@ */ "use strict"; -/** @typedef {{new(): Hash}} HashConstructor */ -/** - * @typedef {Object} Hash - * @property {function(string|Buffer, string=): Hash} update - * @property {function(string): string} digest - */ +const AbstractMethodError = require("../AbstractMethodError"); const BULK_SIZE = 1000; -class BulkUpdateDecorator { +class Hash { + /** + * Update hash {@link https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding} + * @param {string|Buffer} data data + * @param {string=} inputEncoding data encoding + * @returns {this} updated hash + */ + update(data, inputEncoding) { + throw new AbstractMethodError(); + } + + /** + * Calculates the digest {@link https://nodejs.org/api/crypto.html#crypto_hash_digest_encoding} + * @param {string=} encoding encoding of the return value + * @returns {string|Buffer} digest + */ + digest(encoding) { + throw new AbstractMethodError(); + } +} + +exports.Hash = Hash; +/** @typedef {typeof Hash} HashConstructor */ + +class BulkUpdateDecorator extends Hash { + /** + * @param {Hash} hash hash + */ constructor(hash) { + super(); this.hash = hash; this.buffer = ""; } + /** + * Update hash {@link https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding} + * @param {string|Buffer} data data + * @param {string=} inputEncoding data encoding + * @returns {this} updated hash + */ update(data, inputEncoding) { if ( inputEncoding !== undefined || @@ -40,6 +69,11 @@ class BulkUpdateDecorator { return this; } + /** + * Calculates the digest {@link https://nodejs.org/api/crypto.html#crypto_hash_digest_encoding} + * @param {string=} encoding encoding of the return value + * @returns {string|Buffer} digest + */ digest(encoding) { if (this.buffer.length > 0) { this.hash.update(this.buffer); @@ -51,18 +85,32 @@ class BulkUpdateDecorator { } } -/* istanbul ignore next */ -class DebugHash { +/** + * istanbul ignore next + */ +class DebugHash extends Hash { constructor() { + super(); this.string = ""; } + /** + * Update hash {@link https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding} + * @param {string|Buffer} data data + * @param {string=} inputEncoding data encoding + * @returns {this} updated hash + */ update(data, inputEncoding) { if (typeof data !== "string") data = data.toString("utf-8"); this.string += data; return this; } + /** + * Calculates the digest {@link https://nodejs.org/api/crypto.html#crypto_hash_digest_encoding} + * @param {string=} encoding encoding of the return value + * @returns {string|Buffer} digest + */ digest(encoding) { return this.string.replace(/[^a-z0-9]+/gi, m => Buffer.from(m).toString("hex") diff --git a/schemas/WebpackOptions.json b/schemas/WebpackOptions.json index daafe600438..81ad909373a 100644 --- a/schemas/WebpackOptions.json +++ b/schemas/WebpackOptions.json @@ -905,7 +905,7 @@ }, { "instanceof": "Function", - "tsType": "(new () => import('../lib/util/createHash').Hash)" + "tsType": "import('../lib/util/createHash').HashConstructor" } ] }, diff --git a/test/AbstractMethodError.unittest.js b/test/AbstractMethodError.unittest.js new file mode 100644 index 00000000000..862a2860409 --- /dev/null +++ b/test/AbstractMethodError.unittest.js @@ -0,0 +1,27 @@ +"use strict"; + +const AbstractMethodError = require("../lib/AbstractMethodError"); + +describe("WebpackError", () => { + class Foo { + abstractMethod() { + return new AbstractMethodError(); + } + } + + class Child extends Foo {} + + const expectedMessage = "Abstract method $1. Must be overridden."; + + it("Should construct message with caller info", () => { + const fooClassError = new Foo().abstractMethod(); + const childClassError = new Child().abstractMethod(); + + expect(fooClassError.message).toBe( + expectedMessage.replace("$1", "Foo.abstractMethod") + ); + expect(childClassError.message).toBe( + expectedMessage.replace("$1", "Child.abstractMethod") + ); + }); +});