diff --git a/src/Module.ts b/src/Module.ts index 23a54ca39e8..097c1b89745 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -1,4 +1,5 @@ import { IParse, Options as AcornOptions } from 'acorn'; +import walk from 'acorn/dist/walk'; import * as ESTree from 'estree'; import { locate } from 'locate-character'; import MagicString from 'magic-string'; @@ -159,6 +160,38 @@ function handleMissingExport( ); } +// Find syntax nodes following comments +function findNodesAfterComments(ast, commentNodes) { + const state = { + commentIdx: 0, + nodes: [] + }; + + (function c(node, state, override) { + if (state.commentIdx === commentNodes.length) return; + const pos = commentNodes[state.commentIdx].end; + if (node.end < pos) return; + const type = override || node.type; + if (node.start >= pos) { + state.commentIdx++; + state.nodes.push(node); + } + walk.base[type](node, state, c); + })(ast, state); + + return state.nodes; +} + +function markNodePure(node) { + if (node.type === 'ExpressionStatement') { + markNodePure(node.expression); + } else if (node.type === 'CallExpression') { + node.markedPure = true; + } +} + +const pureCommentRegex = /^ ?#__PURE__\s*$/; + export default class Module { type: 'Module'; private graph: Graph; @@ -258,6 +291,11 @@ export default class Module { timeEnd('generate ast', 3); + const pureMarkerComments = this.comments.filter(comment => pureCommentRegex.test(comment.text)); + const nodesAfterPureComments = findNodesAfterComments(this.esTreeAst, pureMarkerComments); + + nodesAfterPureComments.forEach(node => markNodePure(node)); + this.resolvedIds = resolvedIds || Object.create(null); // By default, `id` is the filename. Custom resolvers and loaders diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 2c88d4a9171..efcdce7507e 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -135,6 +135,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt for (const argument of this.arguments) { if (argument.hasEffects(options)) return true; } + if (this.markedPure) return false; return ( this.callee.hasEffects(options) || this.callee.hasEffectsWhenCalledAtPath( diff --git a/test/function/samples/call-marked-pure/_config.js b/test/function/samples/call-marked-pure/_config.js new file mode 100644 index 00000000000..57ce6f0a504 --- /dev/null +++ b/test/function/samples/call-marked-pure/_config.js @@ -0,0 +1,26 @@ +const assert = require('assert'); + +module.exports = { + description: 'functions marked with pure comment do not have effects', + context: { + require(id) { + if (id === 'socks') { + return () => '🧦'; + } + } + }, + code(code) { + assert.ok(code.search(/socks\(\)/) === -1); + }, + warnings: [ + { + code: 'UNRESOLVED_IMPORT', + importer: 'main.js', + message: + "'socks' is imported by main.js, but could not be resolved – treating it as an external dependency", + source: 'socks', + url: + 'https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency' + } + ] +}; diff --git a/test/function/samples/call-marked-pure/main.js b/test/function/samples/call-marked-pure/main.js new file mode 100644 index 00000000000..140c9fce908 --- /dev/null +++ b/test/function/samples/call-marked-pure/main.js @@ -0,0 +1,8 @@ +import socks from 'socks'; + +/*#__PURE__*/ socks(); +/* #__PURE__*/ socks(); +/*#__PURE__ */ socks(); +/* #__PURE__ */ socks(); +// #__PURE__ +socks(); \ No newline at end of file