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

Move pure comment annotation to Graph.contextParse #3981

Merged
Show file tree
Hide file tree
Changes from all 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
36 changes: 30 additions & 6 deletions src/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { BuildPhase } from './utils/buildPhase';
import { errImplicitDependantIsNotIncluded, error } from './utils/error';
import { analyseModuleExecution } from './utils/executionOrder';
import { PluginDriver } from './utils/PluginDriver';
import { markPureCallExpressions } from './utils/pureComments';
import relativeId from './utils/relativeId';
import { timeEnd, timeStart } from './utils/timers';
import { markModuleAndImpureDependenciesAsExecuted } from './utils/traverseStaticDependencies';
Expand Down Expand Up @@ -45,7 +46,6 @@ function normalizeEntryModules(
export default class Graph {
acornParser: typeof acorn.Parser;
cachedModules: Map<string, ModuleJSON>;
contextParse: (code: string, acornOptions?: acorn.Options) => acorn.Node;
deoptimizationTracker: PathTracker;
entryModules: Module[] = [];
moduleLoader: ModuleLoader;
Expand Down Expand Up @@ -77,11 +77,6 @@ export default class Graph {
for (const key of Object.keys(cache)) cache[key][0]++;
}
}
this.contextParse = (code: string, options: Partial<acorn.Options> = {}) =>
this.acornParser.parse(code, {
...(this.options.acorn as acorn.Options),
...options
});

if (watcher) {
this.watchMode = true;
Expand Down Expand Up @@ -117,6 +112,35 @@ export default class Graph {
this.phase = BuildPhase.GENERATE;
}

contextParse(code: string, options: Partial<acorn.Options> = {}) {
const onCommentOrig = options.onComment;
const comments: acorn.Comment[] = [];

if (onCommentOrig && typeof onCommentOrig == 'function') {
options.onComment = (block, text, start, end, ...args) => {
comments.push({type: block ? "Block" : "Line", value: text, start, end});
return onCommentOrig.call(options, block, text, start, end, ...args);
}
} else {
options.onComment = comments;
}

const ast = this.acornParser.parse(code, {
...(this.options.acorn as acorn.Options),
...options
});

if (typeof onCommentOrig == 'object') {
onCommentOrig.push(...comments);
}

options.onComment = onCommentOrig;

markPureCallExpressions(comments, ast);

return ast;
}

getCache(): RollupCache {
// handle plugin cache eviction
for (const name in this.pluginCache) {
Expand Down
70 changes: 29 additions & 41 deletions src/Module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,13 @@ import { getOrCreate } from './utils/getOrCreate';
import { getOriginalLocation } from './utils/getOriginalLocation';
import { makeLegal } from './utils/identifierHelpers';
import { basename, extname } from './utils/path';
import { markPureCallExpressions } from './utils/pureComments';
import relativeId from './utils/relativeId';
import { RenderOptions } from './utils/renderHelpers';
import { SOURCEMAPPING_URL_RE } from './utils/sourceMappingURL';
import { timeEnd, timeStart } from './utils/timers';
import { markModuleAndImpureDependenciesAsExecuted } from './utils/traverseStaticDependencies';
import { MISSING_EXPORT_SHIM_VARIABLE } from './utils/variableNames';

export interface CommentDescription {
block: boolean;
end: number;
start: number;
text: string;
}

interface ImportDescription {
module: Module | ExternalModule;
name: string;
Expand Down Expand Up @@ -122,35 +114,6 @@ export interface AstContext {
warn: (warning: RollupWarning, pos: number) => void;
}

function tryParse(
module: Module,
Parser: typeof acorn.Parser,
acornOptions: acorn.Options
): acorn.Node {
try {
return Parser.parse(module.info.code!, {
...acornOptions,
onComment: (block: boolean, text: string, start: number, end: number) =>
module.comments.push({ block, text, start, end })
});
} catch (err) {
let message = err.message.replace(/ \(\d+:\d+\)$/, '');
if (module.id.endsWith('.json')) {
message += ' (Note that you need @rollup/plugin-json to import JSON files)';
} else if (!module.id.endsWith('.js')) {
message += ' (Note that you need plugins to import files that are not JavaScript)';
}
return module.error(
{
code: 'PARSE_ERROR',
message,
parserError: err
},
err.pos
);
}
}

const MISSING_EXPORT_SHIM_DESCRIPTION: ExportDescription = {
identifier: null,
localName: MISSING_EXPORT_SHIM_VARIABLE
Expand Down Expand Up @@ -218,7 +181,7 @@ export default class Module {
ast: Program | null = null;
chunkFileNames = new Set<string>();
chunkName: string | null = null;
comments: CommentDescription[] = [];
comments: acorn.Comment[] = [];
cycles = new Set<Symbol>();
dependencies = new Set<Module | ExternalModule>();
dynamicDependencies = new Set<Module | ExternalModule>();
Expand Down Expand Up @@ -719,13 +682,12 @@ export default class Module {

this.alwaysRemovedCode = alwaysRemovedCode || [];
if (!ast) {
ast = tryParse(this, this.graph.acornParser, this.options.acorn as acorn.Options);
ast = this.tryParse();
for (const comment of this.comments) {
if (!comment.block && SOURCEMAPPING_URL_RE.test(comment.text)) {
if (comment.type != "Block" && SOURCEMAPPING_URL_RE.test(comment.value)) {
this.alwaysRemovedCode.push([comment.start, comment.end]);
}
}
markPureCallExpressions(this.comments, ast);
}

timeEnd('generate ast', 3);
Expand Down Expand Up @@ -834,6 +796,32 @@ export default class Module {
return null;
}

tryParse(): acorn.Node {
try {
return this.graph.contextParse(this.info.code!, {
onComment: (block: boolean, text: string, start: number, end: number) =>
this.comments.push({ type: block ? "Block" : "Line", value: text, start, end })
});
} catch (err) {
let message = err.message.replace(/ \(\d+:\d+\)$/, '');
if (this.id.endsWith('.json')) {
message += ' (Note that you need @rollup/plugin-json to import JSON files)';
} else if (!this.id.endsWith('.js')) {
message += ' (Note that you need plugins to import files that are not JavaScript)';
}
console.log(err);
return this.error(
{
code: 'PARSE_ERROR',
message,
parserError: err
},
err.pos
);
}
}


updateOptions({
meta,
moduleSideEffects,
Expand Down
2 changes: 1 addition & 1 deletion src/ast/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const keys: {

export function getAndCreateKeys(esTreeNode: GenericEsTreeNode) {
keys[esTreeNode.type] = Object.keys(esTreeNode).filter(
key => typeof esTreeNode[key] === 'object'
key => key !== '_rollupAnnotations' && typeof esTreeNode[key] === 'object'
);
return keys[esTreeNode.type];
}
5 changes: 2 additions & 3 deletions src/ast/nodes/CallExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ import Identifier from './Identifier';
import MemberExpression from './MemberExpression';
import * as NodeType from './NodeType';
import { ExpressionEntity } from './shared/Expression';
import { ExpressionNode, IncludeChildren, INCLUDE_PARAMETERS, NodeBase } from './shared/Node';
import { Annotation, ExpressionNode, IncludeChildren, INCLUDE_PARAMETERS, NodeBase } from './shared/Node';
import SpreadElement from './SpreadElement';
import Super from './Super';

export default class CallExpression extends NodeBase implements DeoptimizableEntity {
annotatedPure?: boolean;
arguments!: (ExpressionNode | SpreadElement)[];
callee!: ExpressionNode | Super;
optional!: boolean;
Expand Down Expand Up @@ -157,7 +156,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt
}
if (
(this.context.options.treeshake as NormalizedTreeshakingOptions).annotations &&
this.annotatedPure
this.annotations?.some((a: Annotation) => a.pure)
)
return false;
return (
Expand Down
5 changes: 2 additions & 3 deletions src/ast/nodes/NewExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { CallOptions } from '../CallOptions';
import { HasEffectsContext } from '../ExecutionContext';
import { EMPTY_PATH, ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker';
import * as NodeType from './NodeType';
import { ExpressionNode, NodeBase } from './shared/Node';
import { Annotation, ExpressionNode, NodeBase } from './shared/Node';

export default class NewExpression extends NodeBase {
annotatedPure?: boolean;
arguments!: ExpressionNode[];
callee!: ExpressionNode;
type!: NodeType.tNewExpression;
Expand All @@ -27,7 +26,7 @@ export default class NewExpression extends NodeBase {
}
if (
(this.context.options.treeshake as NormalizedTreeshakingOptions).annotations &&
this.annotatedPure
this.annotations?.some((a: Annotation) => a.pure)
)
return false;
return (
Expand Down
18 changes: 11 additions & 7 deletions src/ast/nodes/shared/Node.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as acorn from 'acorn';
import { locate } from 'locate-character';
import MagicString from 'magic-string';
import { AstContext, CommentDescription } from '../../../Module';
import { AstContext } from '../../../Module';
import { NodeRenderOptions, RenderOptions } from '../../../utils/renderHelpers';
import { CallOptions } from '../../CallOptions';
import { DeoptimizableEntity } from '../../DeoptimizableEntity';
Expand All @@ -26,9 +26,10 @@ export interface GenericEsTreeNode extends acorn.Node {

export const INCLUDE_PARAMETERS: 'variables' = 'variables';
export type IncludeChildren = boolean | typeof INCLUDE_PARAMETERS;
export interface Annotation {comment?: acorn.Comment, pure?: boolean}

export interface Node extends Entity {
annotations?: CommentDescription[];
annotations?: Annotation[];
context: AstContext;
end: number;
esTreeNode: GenericEsTreeNode;
Expand Down Expand Up @@ -88,6 +89,7 @@ export interface StatementNode extends Node {}
export interface ExpressionNode extends ExpressionEntity, Node {}

export class NodeBase implements ExpressionNode {
annotations?: Annotation[];
context: AstContext;
end!: number;
esTreeNode: acorn.Node;
Expand Down Expand Up @@ -126,7 +128,7 @@ export class NodeBase implements ExpressionNode {
bind() {
for (const key of this.keys) {
const value = (this as GenericEsTreeNode)[key];
if (value === null || key === 'annotations') continue;
if (value === null) continue;
if (Array.isArray(value)) {
for (const child of value) {
if (child !== null) child.bind();
Expand Down Expand Up @@ -165,7 +167,7 @@ export class NodeBase implements ExpressionNode {
hasEffects(context: HasEffectsContext): boolean {
for (const key of this.keys) {
const value = (this as GenericEsTreeNode)[key];
if (value === null || key === 'annotations') continue;
if (value === null) continue;
if (Array.isArray(value)) {
for (const child of value) {
if (child !== null && child.hasEffects(context)) return true;
Expand Down Expand Up @@ -195,7 +197,7 @@ export class NodeBase implements ExpressionNode {
this.included = true;
for (const key of this.keys) {
const value = (this as GenericEsTreeNode)[key];
if (value === null || key === 'annotations') continue;
if (value === null) continue;
if (Array.isArray(value)) {
for (const child of value) {
if (child !== null) child.include(context, includeChildrenRecursively);
Expand Down Expand Up @@ -232,7 +234,9 @@ export class NodeBase implements ExpressionNode {
// That way, we can override this function to add custom initialisation and then call super.parseNode
if (this.hasOwnProperty(key)) continue;
const value = esTreeNode[key];
if (typeof value !== 'object' || value === null || key === 'annotations') {
if (key === '_rollupAnnotations') {
this.annotations = value;
} else if (typeof value !== 'object' || value === null) {
(this as GenericEsTreeNode)[key] = value;
} else if (Array.isArray(value)) {
(this as GenericEsTreeNode)[key] = [];
Expand All @@ -254,7 +258,7 @@ export class NodeBase implements ExpressionNode {
render(code: MagicString, options: RenderOptions) {
for (const key of this.keys) {
const value = (this as GenericEsTreeNode)[key];
if (value === null || key === 'annotations') continue;
if (value === null) continue;
if (Array.isArray(value)) {
for (const child of value) {
if (child !== null) child.render(code, options);
Expand Down
2 changes: 1 addition & 1 deletion src/utils/PluginContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export function getPluginContexts(
const moduleIds = graph.modulesById.keys();
return wrappedModuleIds();
},
parse: graph.contextParse,
parse: graph.contextParse.bind(graph),
resolve(source, importer, { custom, skipSelf } = BLANK) {
return graph.moduleLoader.resolveId(source, importer, custom, skipSelf ? pidx : null);
},
Expand Down
24 changes: 14 additions & 10 deletions src/utils/pureComments.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as acorn from 'acorn';
import { base as basicWalker, BaseWalker } from 'acorn-walk';
import { CallExpression, ExpressionStatement, NewExpression } from '../ast/nodes/NodeType';
import { CommentDescription } from '../Module';
import { Annotation } from '../ast/nodes/shared/Node';

// patch up acorn-walk until class-fields are officially supported
basicWalker.PropertyDefinition = function (node: any, st: any, c: any) {
Expand All @@ -15,7 +15,7 @@ basicWalker.PropertyDefinition = function (node: any, st: any, c: any) {

interface CommentState {
commentIndex: number;
commentNodes: CommentDescription[];
commentNodes: acorn.Comment[];
}

function handlePureAnnotationsOfNode(
Expand All @@ -34,26 +34,30 @@ function handlePureAnnotationsOfNode(
}

function markPureNode(
node: acorn.Node & { annotations?: CommentDescription[] },
comment: CommentDescription
node: acorn.Node & { _rollupAnnotations?: Annotation[] },
comment: acorn.Comment
) {
if (node.annotations) {
node.annotations.push(comment);
if (node._rollupAnnotations) {
node._rollupAnnotations.push({comment});
} else {
node.annotations = [comment];
node._rollupAnnotations = [{comment}];
}
if (node.type === ExpressionStatement) {
node = (node as any).expression;
}
if (node.type === CallExpression || node.type === NewExpression) {
(node as any).annotatedPure = true;
if (node._rollupAnnotations) {
node._rollupAnnotations.push({pure: true});
} else {
node._rollupAnnotations = [{pure: true}];
}
}
}

const pureCommentRegex = /[@#]__PURE__/;
const isPureComment = (comment: CommentDescription) => pureCommentRegex.test(comment.text);
const isPureComment = (comment: acorn.Comment) => pureCommentRegex.test(comment.value);

export function markPureCallExpressions(comments: CommentDescription[], esTreeAst: acorn.Node) {
export function markPureCallExpressions(comments: acorn.Comment[], esTreeAst: acorn.Node) {
handlePureAnnotationsOfNode(esTreeAst, {
commentIndex: 0,
commentNodes: comments.filter(isPureComment)
Expand Down
11 changes: 7 additions & 4 deletions src/utils/treeshakeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ export function treeshakeNode(node: Node, code: MagicString, start: number, end:
code.remove(start, end);
if (node.annotations) {
for (const annotation of node.annotations) {
if (annotation.start < start) {
code.remove(annotation.start, annotation.end);
if (!annotation.comment) {
continue;
}
if (annotation.comment.start < start) {
code.remove(annotation.comment.start, annotation.comment.end);
} else {
return;
}
Expand All @@ -20,8 +23,8 @@ export function removeAnnotations(node: Node, code: MagicString) {
node = node.parent as Node;
}
if (node.annotations) {
for (const annotation of node.annotations) {
code.remove(annotation.start, annotation.end);
for (const annotation of node.annotations.filter((a) => a.comment)) {
code.remove(annotation.comment!.start, annotation.comment!.end);
}
}
}