UNPKG

6.71 kBJavaScriptView Raw
1import { walk } from 'estree-walker';
2import Scope from './ast/Scope.js';
3import attachScopes from './ast/attachScopes.js';
4import getLocation from './utils/getLocation.js';
5
6const modifierNodes = {
7 AssignmentExpression: 'left',
8 UpdateExpression: 'argument'
9};
10
11function isIife ( node, parent ) {
12 return parent && parent.type === 'CallExpression' && node === parent.callee;
13}
14
15function isReference ( node, parent ) {
16 if ( node.type === 'MemberExpression' ) {
17 return !node.computed && isReference( node.object, node );
18 }
19
20 if ( node.type === 'Identifier' ) {
21 // TODO is this right?
22 if ( parent.type === 'MemberExpression' ) return parent.computed || node === parent.object;
23
24 // disregard the `bar` in { bar: foo }
25 if ( parent.type === 'Property' && node !== parent.value ) return false;
26
27 // disregard the `bar` in `class Foo { bar () {...} }`
28 if ( parent.type === 'MethodDefinition' ) return false;
29
30 // disregard the `bar` in `export { foo as bar }`
31 if ( parent.type === 'ExportSpecifier' && node !== parent.local ) return;
32
33 return true;
34 }
35}
36
37class Reference {
38 constructor ( node, scope ) {
39 this.node = node;
40 this.scope = scope;
41
42 this.declaration = null; // bound later
43
44 this.parts = [];
45
46 let root = node;
47 while ( root.type === 'MemberExpression' ) {
48 this.parts.unshift( root.property.name );
49 root = root.object;
50 }
51
52 this.name = root.name;
53
54 this.start = node.start;
55 this.end = node.start + this.name.length; // can be overridden in the case of namespace members
56 this.rewritten = false;
57 }
58}
59
60export default class Statement {
61 constructor ( node, module, start, end ) {
62 this.node = node;
63 this.module = module;
64 this.start = start;
65 this.end = end;
66 this.next = null; // filled in later
67
68 this.scope = new Scope();
69
70 this.references = [];
71 this.stringLiteralRanges = [];
72
73 this.isIncluded = false;
74
75 this.isImportDeclaration = node.type === 'ImportDeclaration';
76 this.isExportDeclaration = /^Export/.test( node.type );
77 this.isReexportDeclaration = this.isExportDeclaration && !!node.source;
78 }
79
80 analyse () {
81 if ( this.isImportDeclaration ) return; // nothing to analyse
82
83 // attach scopes
84 attachScopes( this );
85
86 // attach statement to each top-level declaration,
87 // so we can mark statements easily
88 this.scope.eachDeclaration( ( name, declaration ) => {
89 declaration.statement = this;
90 });
91
92 // find references
93 let { module, references, scope, stringLiteralRanges } = this;
94 let readDepth = 0;
95
96 walk( this.node, {
97 enter ( node, parent ) {
98 // warn about eval
99 if ( node.type === 'CallExpression' && node.callee.name === 'eval' && !scope.contains( 'eval' ) ) {
100 module.bundle.onwarn( `Use of \`eval\` (in ${module.id}) is discouraged, as it may cause issues with minification. See https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval for more details` );
101 }
102
103 // skip re-export declarations
104 if ( node.type === 'ExportNamedDeclaration' && node.source ) return this.skip();
105
106 if ( node.type === 'TemplateElement' ) stringLiteralRanges.push([ node.start, node.end ]);
107 if ( node.type === 'Literal' && typeof node.value === 'string' && /\n/.test( node.raw ) ) {
108 stringLiteralRanges.push([ node.start + 1, node.end - 1 ]);
109 }
110
111 if ( node._scope ) scope = node._scope;
112 if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth += 1;
113
114 // special case – shorthand properties. because node.key === node.value,
115 // we can't differentiate once we've descended into the node
116 if ( node.type === 'Property' && node.shorthand ) {
117 const reference = new Reference( node.key, scope );
118 reference.isShorthandProperty = true; // TODO feels a bit kludgy
119 references.push( reference );
120 return this.skip();
121 }
122
123 let isReassignment;
124
125 if ( parent && parent.type in modifierNodes ) {
126 let subject = parent[ modifierNodes[ parent.type ] ];
127 let depth = 0;
128
129 while ( subject.type === 'MemberExpression' ) {
130 subject = subject.object;
131 depth += 1;
132 }
133
134 const importDeclaration = module.imports[ subject.name ];
135
136 if ( !scope.contains( subject.name ) && importDeclaration ) {
137 const minDepth = importDeclaration.name === '*' ?
138 2 : // cannot do e.g. `namespace.foo = bar`
139 1; // cannot do e.g. `foo = bar`, but `foo.bar = bar` is fine
140
141 if ( depth < minDepth ) {
142 const err = new Error( `Illegal reassignment to import '${subject.name}'` );
143 err.file = module.id;
144 err.loc = getLocation( module.magicString.toString(), subject.start );
145 throw err;
146 }
147 }
148
149 isReassignment = !depth;
150 }
151
152 if ( isReference( node, parent ) ) {
153 // function declaration IDs are a special case – they're associated
154 // with the parent scope
155 const referenceScope = parent.type === 'FunctionDeclaration' && node === parent.id ?
156 scope.parent :
157 scope;
158
159 const reference = new Reference( node, referenceScope );
160 references.push( reference );
161
162 reference.isImmediatelyUsed = !readDepth;
163 reference.isReassignment = isReassignment;
164
165 this.skip(); // don't descend from `foo.bar.baz` into `foo.bar`
166 }
167 },
168 leave ( node, parent ) {
169 if ( node._scope ) scope = scope.parent;
170 if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth -= 1;
171 }
172 });
173 }
174
175 mark () {
176 if ( this.isIncluded ) return; // prevent infinite loops
177 this.isIncluded = true;
178
179 this.references.forEach( reference => {
180 if ( reference.declaration ) reference.declaration.use();
181 });
182 }
183
184 markSideEffect () {
185 if ( this.isIncluded ) return;
186
187 const statement = this;
188 let hasSideEffect = false;
189
190 walk( this.node, {
191 enter ( node, parent ) {
192 if ( /Function/.test( node.type ) && !isIife( node, parent ) ) return this.skip();
193
194 // If this is a top-level call expression, or an assignment to a global,
195 // this statement will need to be marked
196 if ( node.type === 'CallExpression' || node.type === 'NewExpression' ) {
197 hasSideEffect = true;
198 }
199
200 else if ( node.type in modifierNodes ) {
201 let subject = node[ modifierNodes[ node.type ] ];
202 while ( subject.type === 'MemberExpression' ) subject = subject.object;
203
204 const declaration = statement.module.trace( subject.name );
205
206 if ( !declaration || declaration.isExternal || declaration.statement.isIncluded ) {
207 hasSideEffect = true;
208 }
209 }
210
211 if ( hasSideEffect ) this.skip();
212 }
213 });
214
215 if ( hasSideEffect ) statement.mark();
216 return hasSideEffect;
217 }
218
219 source () {
220 return this.module.source.slice( this.start, this.end );
221 }
222
223 toString () {
224 return this.module.magicString.slice( this.start, this.end );
225 }
226}