PureAnnotationsPlugin.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  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 PureAnnotationsWarning = require("../errors/PureAnnotationsWarning");
  12. const { compareStrings } = require("../util/comparators");
  13. const { CompilerHintNotationRegExp } = require("../util/magicComment");
  14. /** @import { PerformanceOptions } from "../../declarations/WebpackOptions" */
  15. /** @import Compiler from "../Compiler" */
  16. /** @import { Program } from "estree" */
  17. /** @import { BuildInfo } from "../Module" */
  18. /** @import JavascriptParser from "../javascript/JavascriptParser" */
  19. /** @import { PureAnnotationDetails } from "../errors/PureAnnotationsWarning" */
  20. const PLUGIN_NAME = "PureAnnotationsPlugin";
  21. // Enough to name the offenders without listing every module.
  22. const MAX_REPORTED_MODULES = 5;
  23. // The only three the parser reads the annotation for; before anything else it
  24. // is just a comment. Kept in step with `JavascriptParser.isPure`.
  25. const ANNOTATED_TYPES = new Set([
  26. "CallExpression",
  27. "NewExpression",
  28. "TaggedTemplateExpression"
  29. ]);
  30. /**
  31. * Collects where every node starts, and which of those starts carry a node the
  32. * annotation is read for.
  33. * @param {Program} ast the program
  34. * @returns {{ starts: number[], annotated: Set<number> }} the two of them
  35. */
  36. const collectStarts = (ast) => {
  37. /** @type {number[]} */
  38. const starts = [];
  39. /** @type {Set<number>} */
  40. const annotated = new Set();
  41. /** @type {unknown[]} */
  42. const queue = [ast];
  43. while (queue.length > 0) {
  44. const node = queue.pop();
  45. if (!node || typeof node !== "object") continue;
  46. if (Array.isArray(node)) {
  47. for (const item of /** @type {unknown[]} */ (node)) queue.push(item);
  48. continue;
  49. }
  50. // Walked structurally rather than per node type, so a node the estree
  51. // types do not name is still reached.
  52. const fields = /** @type {Record<string, unknown>} */ (node);
  53. const { type, range } = fields;
  54. const start = Array.isArray(range)
  55. ? /** @type {unknown[]} */ (range)[0]
  56. : undefined;
  57. if (typeof type === "string" && typeof start === "number") {
  58. starts.push(start);
  59. if (ANNOTATED_TYPES.has(type)) annotated.add(start);
  60. }
  61. for (const key of Object.keys(fields)) {
  62. if (key !== "range" && key !== "loc") queue.push(fields[key]);
  63. }
  64. }
  65. // Sorted so each annotation can binary-search for the node after it rather
  66. // than walking every start, which is quadratic on a generated file.
  67. starts.sort((a, b) => a - b);
  68. return { starts, annotated };
  69. };
  70. class PureAnnotationsPlugin {
  71. /**
  72. * Creates an instance of PureAnnotationsPlugin.
  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. */
  93. const handler = (parser) => {
  94. parser.hooks.program.tap(PLUGIN_NAME, (ast, comments) => {
  95. const pure = comments.filter((comment) =>
  96. CompilerHintNotationRegExp.Pure.test(comment.value)
  97. );
  98. if (pure.length === 0) return;
  99. const { starts, annotated } = collectStarts(ast);
  100. let wasted = 0;
  101. for (const comment of pure) {
  102. const range = /** @type {[number, number]} */ (comment.range);
  103. const after = range[1];
  104. // Comments are outside every node's range, so the node the
  105. // annotation applies to is whichever starts first after it.
  106. let low = 0;
  107. let high = starts.length;
  108. while (low < high) {
  109. const middle = (low + high) >> 1;
  110. if (starts[middle] < after) {
  111. low = middle + 1;
  112. } else {
  113. high = middle;
  114. }
  115. }
  116. if (low === starts.length || !annotated.has(starts[low])) {
  117. wasted++;
  118. }
  119. }
  120. if (wasted === 0) return;
  121. // Kept on the module rather than in a map here, so a module the
  122. // filesystem cache restores rather than parses still reports.
  123. const buildInfo = /** @type {BuildInfo} */ (
  124. parser.state.module.buildInfo
  125. );
  126. buildInfo.ineffectivePureAnnotations = wasted;
  127. });
  128. };
  129. normalModuleFactory.hooks.parser
  130. .for(JAVASCRIPT_MODULE_TYPE_AUTO)
  131. .tap(PLUGIN_NAME, handler);
  132. normalModuleFactory.hooks.parser
  133. .for(JAVASCRIPT_MODULE_TYPE_ESM)
  134. .tap(PLUGIN_NAME, handler);
  135. normalModuleFactory.hooks.parser
  136. .for(JAVASCRIPT_MODULE_TYPE_DYNAMIC)
  137. .tap(PLUGIN_NAME, handler);
  138. // `afterSeal` is past the hash, which folds every message into it — a
  139. // hint reported earlier would change the build's identity.
  140. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  141. const { requestShortener } = compilation;
  142. /** @type {PureAnnotationDetails[]} */
  143. const wasteful = [];
  144. let total = 0;
  145. for (const module of compilation.modules) {
  146. // Every built module carries one, so only the field is in question.
  147. const buildInfo = /** @type {BuildInfo} */ (module.buildInfo);
  148. if (!buildInfo.ineffectivePureAnnotations) continue;
  149. const count = buildInfo.ineffectivePureAnnotations;
  150. total += count;
  151. wasteful.push({
  152. name: module.readableIdentifier(requestShortener),
  153. count
  154. });
  155. }
  156. if (wasteful.length === 0) return;
  157. // Ties break by name: module order is not stable across runs.
  158. wasteful.sort(
  159. (a, b) => b.count - a.count || compareStrings(a.name, b.name)
  160. );
  161. const warning = new PureAnnotationsWarning(
  162. wasteful.slice(0, MAX_REPORTED_MODULES),
  163. total
  164. );
  165. if (hints === "error") {
  166. compilation.errors.push(warning);
  167. } else if (hints === "stats") {
  168. compilation.hints.push(warning);
  169. } else {
  170. compilation.warnings.push(warning);
  171. }
  172. });
  173. }
  174. );
  175. }
  176. }
  177. module.exports = PureAnnotationsPlugin;