TopLevelThisPlugin.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. const {
  7. JAVASCRIPT_MODULE_TYPE_AUTO,
  8. JAVASCRIPT_MODULE_TYPE_DYNAMIC,
  9. JAVASCRIPT_MODULE_TYPE_ESM
  10. } = require("../ModuleTypeConstants");
  11. const TopLevelThisWarning = require("../errors/TopLevelThisWarning");
  12. const { compareStrings } = require("../util/comparators");
  13. const getSourceModules = require("./getSourceModules");
  14. /** @import { Program } from "estree" */
  15. /** @import { PerformanceOptions } from "../../declarations/WebpackOptions" */
  16. /** @import Compiler from "../Compiler" */
  17. /** @import Module from "../Module" */
  18. /** @import { BuildInfo } from "../Module" */
  19. /** @import JavascriptParser from "../javascript/JavascriptParser" */
  20. /** @import { TopLevelThisDetails } from "../errors/TopLevelThisWarning" */
  21. const PLUGIN_NAME = "TopLevelThisPlugin";
  22. // Enough to name the offenders without listing every module.
  23. const MAX_REPORTED_MODULES = 5;
  24. // Bodies that rebind `this`; everything else keeps the enclosing one, so an
  25. // arrow function at the top level still reads the module's `this`.
  26. const REBINDS_THIS = new Set(["FunctionDeclaration", "FunctionExpression"]);
  27. // A class rebinds `this` in its body, but its heritage clause and computed
  28. // keys are evaluated where the class is written.
  29. const CLASS_TYPES = new Set(["ClassDeclaration", "ClassExpression"]);
  30. /**
  31. * Counts the reads of `this` that reach the top level of a program.
  32. * @param {Program} ast the program
  33. * @returns {number} how many there are
  34. */
  35. const countTopLevelThis = (ast) => {
  36. let count = 0;
  37. /** @type {unknown[]} */
  38. const queue = [ast];
  39. while (queue.length > 0) {
  40. const node = queue.pop();
  41. if (!node || typeof node !== "object") continue;
  42. if (Array.isArray(node)) {
  43. for (const item of /** @type {unknown[]} */ (node)) queue.push(item);
  44. continue;
  45. }
  46. // Walked structurally rather than per node type, so a node the estree
  47. // types do not name is still reached.
  48. const fields = /** @type {Record<string, unknown>} */ (node);
  49. const { type } = fields;
  50. if (typeof type !== "string") continue;
  51. if (REBINDS_THIS.has(type)) continue;
  52. if (type === "ThisExpression") {
  53. count++;
  54. continue;
  55. }
  56. if (CLASS_TYPES.has(type)) {
  57. queue.push(fields.superClass);
  58. const body = /** @type {EXPECTED_ANY} */ (fields.body);
  59. for (const element of (body && body.body) || []) {
  60. if (element.computed) queue.push(element.key);
  61. }
  62. continue;
  63. }
  64. for (const key of Object.keys(fields)) {
  65. if (key !== "range" && key !== "loc") queue.push(fields[key]);
  66. }
  67. }
  68. return count;
  69. };
  70. class TopLevelThisPlugin {
  71. /**
  72. * Creates an instance of TopLevelThisPlugin.
  73. * @param {PerformanceOptions} options the plugin options
  74. */
  75. constructor(options) {
  76. /** @type {PerformanceOptions["hints"]} */
  77. this.hints = options.hints;
  78. }
  79. /**
  80. * Applies the plugin by registering its hooks on the compiler.
  81. * @param {Compiler} compiler the compiler instance
  82. * @returns {void}
  83. */
  84. apply(compiler) {
  85. const hints = this.hints;
  86. if (!hints) return;
  87. compiler.hooks.compilation.tap(
  88. PLUGIN_NAME,
  89. (compilation, { normalModuleFactory }) => {
  90. /**
  91. * @param {JavascriptParser} parser the parser
  92. * @returns {void}
  93. */
  94. const handler = (parser) => {
  95. parser.hooks.program.tap(PLUGIN_NAME, (ast) => {
  96. // Only ES modules: in CommonJS `this` is `module.exports`, which
  97. // is what the code reading it means.
  98. const buildMeta = parser.state.module.buildMeta;
  99. if (!buildMeta || buildMeta.exportsType !== "namespace") return;
  100. const count = countTopLevelThis(ast);
  101. if (count === 0) return;
  102. // Kept on the module rather than in a map here, so a module the
  103. // filesystem cache restores rather than parses still reports.
  104. const buildInfo =
  105. /** @type {BuildInfo} */
  106. (parser.state.module.buildInfo);
  107. buildInfo.topLevelThis = count;
  108. });
  109. };
  110. normalModuleFactory.hooks.parser
  111. .for(JAVASCRIPT_MODULE_TYPE_AUTO)
  112. .tap(PLUGIN_NAME, handler);
  113. normalModuleFactory.hooks.parser
  114. .for(JAVASCRIPT_MODULE_TYPE_ESM)
  115. .tap(PLUGIN_NAME, handler);
  116. normalModuleFactory.hooks.parser
  117. .for(JAVASCRIPT_MODULE_TYPE_DYNAMIC)
  118. .tap(PLUGIN_NAME, handler);
  119. // `afterSeal` is past the hash, which folds every message into it — a
  120. // hint reported earlier would change the build's identity.
  121. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  122. /** @type {TopLevelThisDetails[]} */
  123. const modules = [];
  124. let total = 0;
  125. // A module can be reached both on its own and inside a
  126. // concatenation, so it is only counted the first time.
  127. /** @type {Set<Module>} */
  128. const seen = new Set();
  129. for (const parent of compilation.modules) {
  130. // Scope hoisting makes several modules into one, and the evidence
  131. // sits on the ones that were parsed.
  132. for (const module of getSourceModules(parent)) {
  133. if (seen.has(module)) continue;
  134. seen.add(module);
  135. const buildInfo = /** @type {BuildInfo} */ (module.buildInfo);
  136. const count = buildInfo.topLevelThis;
  137. if (!count) continue;
  138. total += count;
  139. modules.push({
  140. name: module.readableIdentifier(compilation.requestShortener),
  141. count
  142. });
  143. }
  144. }
  145. if (modules.length === 0) return;
  146. // Most first; ties break by name, module order is not stable.
  147. modules.sort(
  148. (a, b) => b.count - a.count || compareStrings(a.name, b.name)
  149. );
  150. const warning = new TopLevelThisWarning(
  151. modules.slice(0, MAX_REPORTED_MODULES),
  152. total
  153. );
  154. if (hints === "error") {
  155. compilation.errors.push(warning);
  156. } else if (hints === "stats") {
  157. compilation.hints.push(warning);
  158. } else {
  159. compilation.warnings.push(warning);
  160. }
  161. });
  162. }
  163. );
  164. }
  165. }
  166. module.exports = TopLevelThisPlugin;