/// /** * A lint rule that stops code from spreading a collection into a call that * takes one argument per element. * * `records.push(...more)` passes every element of `more` as a separate * argument, and V8 limits how many arguments one call can take: past roughly a * hundred thousand, the call throws `RangeError: Maximum call stack size * exceeded`. A collection that grows with data — test records, the changes a * cell write makes, the lines of a file — makes a spread that is safe on the * day it is written fail later, once the data grows past the limit. The rule * cannot tell such a collection from one whose size the code fixes, so it * reports both. * * The rule reports a spread argument to a method named `push`, `unshift`, or * `splice`, whatever it is called on, and to `Math.max`, `Math.min`, * `String.fromCharCode`, and `String.fromCodePoint`. Those are the calls whose * purpose is to take a whole collection as arguments. A spread into any other * call, such as `path.join(root, ...segments)`, forwards a list whose length * the code fixes, and is left alone. So is a spread into an array literal, * which V8 builds without passing arguments. A spread that has to stay * carries a `deno-lint-ignore` comment saying why. */ /** The methods reported whatever object they are called on. */ const METHODS: ReadonlySet = new Set(["push", "splice", "unshift"]); /** The functions reported when called on the global named by the key. */ const FUNCTIONS: ReadonlyMap> = new Map([ ["Math", new Set(["max", "min"])], ["String", new Set(["fromCharCode", "fromCodePoint"])], ]); const MESSAGE = "Spreading a collection into this call passes each element as a separate " + "argument, and V8 throws `RangeError: Maximum call stack size exceeded` " + "once a call has more than about a hundred thousand of them. Append in a " + "loop (`for (const item of items) out.push(item);`), or to an array cell " + "with `pushAll(items)`; take the largest or smallest with `maxOf` or " + "`minOf` from `@commonfabric/utils/math`; and replace a range with " + "`spliceAll` from `@commonfabric/utils/arrays`. See " + "docs/development/DEVELOPMENT.md, " + '"Spreading a collection into a call".'; /** The name of an identifier, or undefined for any other node. */ function nameOf(node: Deno.lint.Node): string | undefined { return node.type === "Identifier" ? node.name : undefined; } /** Returns true when `callee` is one of the calls this rule reports. */ function takesCollection(callee: Deno.lint.Node): boolean { // `held?.push(...)` wraps its callee in a `ChainExpression`. if (callee.type === "ChainExpression") callee = callee.expression; if (callee.type !== "MemberExpression" || callee.computed) return false; const method = nameOf(callee.property); if (method === undefined) return false; if (METHODS.has(method)) return true; const object = nameOf(callee.object); return object !== undefined && FUNCTIONS.get(object)?.has(method) === true; } export default { name: "cf-spread", rules: { "no-spread-arguments": { create(context) { return { CallExpression(node) { if (!takesCollection(node.callee)) return; for (const argument of node.arguments) { if (argument.type !== "SpreadElement") continue; // The node of a spread covers only its `...`, so the report // runs on to the end of the collection it spreads. const range: Deno.lint.Range = [ argument.range[0], argument.argument.range[1], ]; context.report({ range, message: MESSAGE }); } }, }; }, }, }, } satisfies Deno.lint.Plugin;