| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Alexander Akait @alexander-akait
- */
- "use strict";
- const {
- JAVASCRIPT_MODULE_TYPE_AUTO,
- JAVASCRIPT_MODULE_TYPE_DYNAMIC,
- JAVASCRIPT_MODULE_TYPE_ESM
- } = require("../ModuleTypeConstants");
- const PureAnnotationsWarning = require("../errors/PureAnnotationsWarning");
- const { compareStrings } = require("../util/comparators");
- const { CompilerHintNotationRegExp } = require("../util/magicComment");
- /** @import { PerformanceOptions } from "../../declarations/WebpackOptions" */
- /** @import Compiler from "../Compiler" */
- /** @import { Program } from "estree" */
- /** @import { BuildInfo } from "../Module" */
- /** @import JavascriptParser from "../javascript/JavascriptParser" */
- /** @import { PureAnnotationDetails } from "../errors/PureAnnotationsWarning" */
- const PLUGIN_NAME = "PureAnnotationsPlugin";
- // Enough to name the offenders without listing every module.
- const MAX_REPORTED_MODULES = 5;
- // The only three the parser reads the annotation for; before anything else it
- // is just a comment. Kept in step with `JavascriptParser.isPure`.
- const ANNOTATED_TYPES = new Set([
- "CallExpression",
- "NewExpression",
- "TaggedTemplateExpression"
- ]);
- /**
- * Collects where every node starts, and which of those starts carry a node the
- * annotation is read for.
- * @param {Program} ast the program
- * @returns {{ starts: number[], annotated: Set<number> }} the two of them
- */
- const collectStarts = (ast) => {
- /** @type {number[]} */
- const starts = [];
- /** @type {Set<number>} */
- const annotated = new Set();
- /** @type {unknown[]} */
- const queue = [ast];
- while (queue.length > 0) {
- const node = queue.pop();
- if (!node || typeof node !== "object") continue;
- if (Array.isArray(node)) {
- for (const item of /** @type {unknown[]} */ (node)) queue.push(item);
- continue;
- }
- // Walked structurally rather than per node type, so a node the estree
- // types do not name is still reached.
- const fields = /** @type {Record<string, unknown>} */ (node);
- const { type, range } = fields;
- const start = Array.isArray(range)
- ? /** @type {unknown[]} */ (range)[0]
- : undefined;
- if (typeof type === "string" && typeof start === "number") {
- starts.push(start);
- if (ANNOTATED_TYPES.has(type)) annotated.add(start);
- }
- for (const key of Object.keys(fields)) {
- if (key !== "range" && key !== "loc") queue.push(fields[key]);
- }
- }
- // Sorted so each annotation can binary-search for the node after it rather
- // than walking every start, which is quadratic on a generated file.
- starts.sort((a, b) => a - b);
- return { starts, annotated };
- };
- class PureAnnotationsPlugin {
- /**
- * Creates an instance of PureAnnotationsPlugin.
- * @param {PerformanceOptions} options the plugin options
- */
- constructor(options) {
- /** @type {PerformanceOptions["hints"]} */
- this.hints = options.hints;
- }
- /**
- * Applies the plugin by registering its hooks on the compiler.
- * @param {Compiler} compiler the compiler instance
- * @returns {void}
- */
- apply(compiler) {
- const hints = this.hints;
- if (!hints) return;
- compiler.hooks.compilation.tap(
- PLUGIN_NAME,
- (compilation, { normalModuleFactory }) => {
- /**
- * @param {JavascriptParser} parser the parser
- */
- const handler = (parser) => {
- parser.hooks.program.tap(PLUGIN_NAME, (ast, comments) => {
- const pure = comments.filter((comment) =>
- CompilerHintNotationRegExp.Pure.test(comment.value)
- );
- if (pure.length === 0) return;
- const { starts, annotated } = collectStarts(ast);
- let wasted = 0;
- for (const comment of pure) {
- const range = /** @type {[number, number]} */ (comment.range);
- const after = range[1];
- // Comments are outside every node's range, so the node the
- // annotation applies to is whichever starts first after it.
- let low = 0;
- let high = starts.length;
- while (low < high) {
- const middle = (low + high) >> 1;
- if (starts[middle] < after) {
- low = middle + 1;
- } else {
- high = middle;
- }
- }
- if (low === starts.length || !annotated.has(starts[low])) {
- wasted++;
- }
- }
- if (wasted === 0) return;
- // Kept on the module rather than in a map here, so a module the
- // filesystem cache restores rather than parses still reports.
- const buildInfo = /** @type {BuildInfo} */ (
- parser.state.module.buildInfo
- );
- buildInfo.ineffectivePureAnnotations = wasted;
- });
- };
- normalModuleFactory.hooks.parser
- .for(JAVASCRIPT_MODULE_TYPE_AUTO)
- .tap(PLUGIN_NAME, handler);
- normalModuleFactory.hooks.parser
- .for(JAVASCRIPT_MODULE_TYPE_ESM)
- .tap(PLUGIN_NAME, handler);
- normalModuleFactory.hooks.parser
- .for(JAVASCRIPT_MODULE_TYPE_DYNAMIC)
- .tap(PLUGIN_NAME, handler);
- // `afterSeal` is past the hash, which folds every message into it — a
- // hint reported earlier would change the build's identity.
- compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
- const { requestShortener } = compilation;
- /** @type {PureAnnotationDetails[]} */
- const wasteful = [];
- let total = 0;
- for (const module of compilation.modules) {
- // Every built module carries one, so only the field is in question.
- const buildInfo = /** @type {BuildInfo} */ (module.buildInfo);
- if (!buildInfo.ineffectivePureAnnotations) continue;
- const count = buildInfo.ineffectivePureAnnotations;
- total += count;
- wasteful.push({
- name: module.readableIdentifier(requestShortener),
- count
- });
- }
- if (wasteful.length === 0) return;
- // Ties break by name: module order is not stable across runs.
- wasteful.sort(
- (a, b) => b.count - a.count || compareStrings(a.name, b.name)
- );
- const warning = new PureAnnotationsWarning(
- wasteful.slice(0, MAX_REPORTED_MODULES),
- total
- );
- if (hints === "error") {
- compilation.errors.push(warning);
- } else if (hints === "stats") {
- compilation.hints.push(warning);
- } else {
- compilation.warnings.push(warning);
- }
- });
- }
- );
- }
- }
- module.exports = PureAnnotationsPlugin;
|