Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support #__NO_SIDE_EFFECTS__ annotation for function declaration #5024

Merged
merged 11 commits into from Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Graph.ts
Expand Up @@ -17,14 +17,14 @@ import type {
import { PluginDriver } from './utils/PluginDriver';
import Queue from './utils/Queue';
import { BuildPhase } from './utils/buildPhase';
import { addAnnotations } from './utils/commentAnnotations';
import {
error,
errorCircularDependency,
errorImplicitDependantIsNotIncluded,
errorMissingExport
} from './utils/error';
import { analyseModuleExecution } from './utils/executionOrder';
import { addAnnotations } from './utils/pureComments';
import { getPureFunctions } from './utils/pureFunctions';
import type { PureFunctions } from './utils/pureFunctions';
import { timeEnd, timeStart } from './utils/timers';
Expand Down
8 changes: 8 additions & 0 deletions src/ast/nodes/ArrowFunctionExpression.ts
@@ -1,3 +1,4 @@
import type { NormalizedTreeshakingOptions } from '../../rollup/types';
import type { HasEffectsContext, InclusionContext } from '../ExecutionContext';
import type { NodeInteraction } from '../NodeInteractions';
import { INTERACTION_CALLED } from '../NodeInteractions';
Expand Down Expand Up @@ -38,6 +39,13 @@ export default class ArrowFunctionExpression extends FunctionBase {
): boolean {
if (super.hasEffectsOnInteractionAtPath(path, interaction, context)) return true;
if (interaction.type === INTERACTION_CALLED) {
if (
(this.context.options.treeshake as NormalizedTreeshakingOptions).annotations &&
this.annotationNoSideEffects
) {
return false;
}

const { ignore, brokenFlow } = context;
context.ignore = {
breaks: false,
Expand Down
2 changes: 1 addition & 1 deletion src/ast/nodes/CallExpression.ts
Expand Up @@ -59,7 +59,7 @@ export default class CallExpression
}
if (
(this.context.options.treeshake as NormalizedTreeshakingOptions).annotations &&
this.annotations
this.annotationPure
)
return false;
return (
Expand Down
2 changes: 1 addition & 1 deletion src/ast/nodes/NewExpression.ts
Expand Up @@ -23,7 +23,7 @@ export default class NewExpression extends NodeBase {
}
if (
(this.context.options.treeshake as NormalizedTreeshakingOptions).annotations &&
this.annotations
this.annotationPure
) {
return false;
}
Expand Down
17 changes: 17 additions & 0 deletions src/ast/nodes/shared/FunctionNode.ts
@@ -1,3 +1,4 @@
import type { NormalizedTreeshakingOptions } from '../../../rollup/types';
import { type HasEffectsContext, type InclusionContext } from '../../ExecutionContext';
import type { NodeInteraction } from '../../NodeInteractions';
import { INTERACTION_CALLED } from '../../NodeInteractions';
Expand Down Expand Up @@ -45,6 +46,14 @@ export default class FunctionNode extends FunctionBase {

hasEffects(context: HasEffectsContext): boolean {
if (!this.deoptimized) this.applyDeoptimizations();

if (
(this.context.options.treeshake as NormalizedTreeshakingOptions).annotations &&
this.annotationNoSideEffects
) {
return false;
}

return !!this.id?.hasEffects(context);
}

Expand All @@ -54,6 +63,14 @@ export default class FunctionNode extends FunctionBase {
context: HasEffectsContext
): boolean {
if (super.hasEffectsOnInteractionAtPath(path, interaction, context)) return true;

if (
(this.context.options.treeshake as NormalizedTreeshakingOptions).annotations &&
this.annotationNoSideEffects
) {
return false;
}

if (interaction.type === INTERACTION_CALLED) {
const thisInit = context.replacedVariableInits.get(this.scope.thisVariable);
context.replacedVariableInits.set(
Expand Down
16 changes: 14 additions & 2 deletions src/ast/nodes/shared/Node.ts
Expand Up @@ -2,7 +2,8 @@ import type * as acorn from 'acorn';
import { locate, type Location } from 'locate-character';
import type MagicString from 'magic-string';
import type { AstContext } from '../../../Module';
import { ANNOTATION_KEY, INVALID_COMMENT_KEY } from '../../../utils/pureComments';
import type { RollupAnnotation } from '../../../utils/commentAnnotations';
import { ANNOTATION_KEY, INVALID_COMMENT_KEY } from '../../../utils/commentAnnotations';
import type { NodeRenderOptions, RenderOptions } from '../../../utils/renderHelpers';
import type { DeoptimizableEntity } from '../../DeoptimizableEntity';
import type { Entity } from '../../Entity';
Expand Down Expand Up @@ -124,7 +125,12 @@ export interface ChainElement extends ExpressionNode {
}

export class NodeBase extends ExpressionEntity implements ExpressionNode {
declare annotations?: acorn.Comment[];
/** Marked with #__NO_SIDE_EFFECTS__ annotation */
declare annotationNoSideEffects?: boolean;
/** Marked with #__PURE__ annotation */
declare annotationPure?: boolean;
declare annotations?: RollupAnnotation[];

context: AstContext;
declare end: number;
esTreeNode: acorn.Node | null;
Expand Down Expand Up @@ -263,6 +269,12 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode {
if (key.charCodeAt(0) === 95 /* _ */) {
if (key === ANNOTATION_KEY) {
this.annotations = value;
antfu marked this conversation as resolved.
Show resolved Hide resolved
this.annotationNoSideEffects = this.annotations!.some(
antfu marked this conversation as resolved.
Show resolved Hide resolved
comment => comment.annotationType === 'noSideEffects'
);
this.annotationPure = this.annotations!.some(
comment => comment.annotationType === 'pure'
);
} else if (key === INVALID_COMMENT_KEY) {
for (const { start, end } of value as acorn.Comment[])
this.context.magicString.remove(start, end);
Expand Down
56 changes: 49 additions & 7 deletions src/utils/pureComments.ts → src/utils/commentAnnotations.ts
@@ -1,20 +1,32 @@
import type * as acorn from 'acorn';
import { base as basicWalker } from 'acorn-walk';
import {
ArrowFunctionExpression,
BinaryExpression,
CallExpression,
ChainExpression,
ConditionalExpression,
ExportDefaultDeclaration,
ExportNamedDeclaration,
ExpressionStatement,
FunctionDeclaration,
LogicalExpression,
NewExpression,
SequenceExpression
SequenceExpression,
VariableDeclaration,
VariableDeclarator
} from '../ast/nodes/NodeType';
import { SOURCEMAPPING_URL_RE } from './sourceMappingURL';

export type AnnotationType = 'noSideEffects' | 'pure';

export interface RollupAnnotation extends acorn.Comment {
annotationType: AnnotationType;
}

interface CommentState {
annotationIndex: number;
annotations: acorn.Comment[];
annotations: RollupAnnotation[];
code: string;
}

Expand Down Expand Up @@ -93,11 +105,34 @@
invalidAnnotation = true;
break;
}
case ExportNamedDeclaration:
case ExportDefaultDeclaration: {
node = (node as any).declaration;
continue;
}
case VariableDeclaration: {
// case: /*#__PURE__*/ const foo = () => {}
const declaration = node as any;
if (declaration.kind === 'const') {
// jsdoc only applies to the first declaration
node = declaration.declarations[0].init;
continue;
}
invalidAnnotation = true;
antfu marked this conversation as resolved.
Show resolved Hide resolved
break;

Check warning on line 122 in src/utils/commentAnnotations.ts

View check run for this annotation

Codecov / codecov/patch

src/utils/commentAnnotations.ts#L121-L122

Added lines #L121 - L122 were not covered by tests
}
case VariableDeclarator: {
node = (node as any).init;
continue;
}
case FunctionDeclaration:
case ArrowFunctionExpression:
case CallExpression:
case NewExpression: {
break;
}
default: {
console.log({ node });
invalidAnnotation = true;
}
}
Expand Down Expand Up @@ -134,25 +169,32 @@
return true;
}

const pureCommentRegex = /[#@]__PURE__/;
const annotationsRegexes: [AnnotationType, RegExp][] = [
['pure', /[#@]__PURE__/],
['noSideEffects', /[#@]__NO_SIDE_EFFECTS__/]
];

export function addAnnotations(
comments: readonly acorn.Comment[],
esTreeAst: acorn.Node,
code: string
): void {
const annotations: acorn.Comment[] = [];
const annotations: RollupAnnotation[] = [];
const sourceMappingComments: acorn.Comment[] = [];
for (const comment of comments) {
if (pureCommentRegex.test(comment.value)) {
annotations.push(comment);
} else if (SOURCEMAPPING_URL_RE.test(comment.value)) {
for (const [annotationType, regex] of annotationsRegexes) {
if (regex.test(comment.value)) {
annotations.push({ ...comment, annotationType });
}
}
if (SOURCEMAPPING_URL_RE.test(comment.value)) {
sourceMappingComments.push(comment);
}
}
for (const comment of sourceMappingComments) {
annotateNode(esTreeAst, comment, false);
}

handlePureAnnotationsOfNode(esTreeAst, {
annotationIndex: 0,
annotations,
Expand Down
@@ -0,0 +1,5 @@
// tests compiled from https://github.com/mishoo/UglifyJS2/blob/88c8f4e363e0d585b33ea29df560243d3dc74ce1/test/compress/pure_funcs.js

module.exports = defineTest({
description: 'preserve __NO_SIDE_EFFECTS__ annotations for function declarations'
});
@@ -0,0 +1,70 @@
/*#__NO_SIDE_EFFECTS__*/
function fnFromSub (args) {
console.log(args);
return args
}

function fnPure(args) {
return args
}

function fnEffects(args) {
console.log(args);
return args
}

/*#__NO_SIDE_EFFECTS__*/
function fnA (args) {
console.log(args);
return args
}

/*#__NO_SIDE_EFFECTS__*/
function fnB (args) {
console.log(args);
return args
}

const fnC = /*#__NO_SIDE_EFFECTS__*/ (args) => {
console.log(args);
return args
};


/*#__NO_SIDE_EFFECTS__*/
const fnD = (args) => {
console.log(args);
return args
};

/*#__NO_SIDE_EFFECTS__*/
const fnE = (args) => {
console.log(args);
return args
};

/**
* This is a jsdoc comment, with no side effects annotation
*
* @param {any} args
* @__NO_SIDE_EFFECTS__
*/
const fnF = (args) => {
console.log(args);
return args
};

const fnAlias = fnA;

/**
* Have both annotations
*
* @__PURE__
* @__NO_SIDE_EFFECTS__
*/
const fnBothAnnotations = (args) => {
console.log(args);
return args
};

export { fnA, fnAlias, fnB, fnBothAnnotations, fnC, fnD, fnE, fnEffects, fnF, fnFromSub, fnPure };
@@ -0,0 +1,73 @@
export function fnPure(args) {
return args
}

export function fnEffects(args) {
console.log(args)
return args
}

/*#__NO_SIDE_EFFECTS__*/
function fnA (args) {
console.log(args)
return args
}
export { fnA }

/*#__NO_SIDE_EFFECTS__*/
export function fnB (args) {
console.log(args)
return args
}

export const fnC = /*#__NO_SIDE_EFFECTS__*/ (args) => {
console.log(args)
return args
}


/*#__NO_SIDE_EFFECTS__*/
const fnD = (args) => {
console.log(args)
return args
}

export { fnD }

/*#__NO_SIDE_EFFECTS__*/
export const fnE = (args) => {
console.log(args)
return args
}

/**
* This is a jsdoc comment, with no side effects annotation
*
* @param {any} args
* @__NO_SIDE_EFFECTS__
*/
export const fnF = (args) => {
console.log(args)
return args
}

/*#__NO_SIDE_EFFECTS__*/
export default function fnDefault(args) {
console.log(args)
return args
}

export * from './sub-functions'

export const fnAlias = fnA

/**
* Have both annotations
*
* @__PURE__
* @__NO_SIDE_EFFECTS__
*/
export const fnBothAnnotations = (args) => {
console.log(args)
return args
}
@@ -0,0 +1 @@
export * from './functions'
@@ -0,0 +1,5 @@
/*#__NO_SIDE_EFFECTS__*/
export function fnFromSub (args) {
console.log(args)
return args
}
@@ -0,0 +1,5 @@
// tests compiled from https://github.com/mishoo/UglifyJS2/blob/88c8f4e363e0d585b33ea29df560243d3dc74ce1/test/compress/pure_funcs.js

module.exports = defineTest({
description: '__NO_SIDE_EFFECTS__ annotations for function declarations'
});