UNPKG

6.59 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 if ( node.type === 'TemplateElement' ) stringLiteralRanges.push([ node.start, node.end ]);
104 if ( node.type === 'Literal' && typeof node.value === 'string' && /\n/.test( node.raw ) ) {
105 stringLiteralRanges.push([ node.start + 1, node.end - 1 ]);
106 }
107
108 if ( node._scope ) scope = node._scope;
109 if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth += 1;
110
111 // special case – shorthand properties. because node.key === node.value,
112 // we can't differentiate once we've descended into the node
113 if ( node.type === 'Property' && node.shorthand ) {
114 const reference = new Reference( node.key, scope );
115 reference.isShorthandProperty = true; // TODO feels a bit kludgy
116 references.push( reference );
117 return this.skip();
118 }
119
120 let isReassignment;
121
122 if ( parent && parent.type in modifierNodes ) {
123 let subject = parent[ modifierNodes[ parent.type ] ];
124 let depth = 0;
125
126 while ( subject.type === 'MemberExpression' ) {
127 subject = subject.object;
128 depth += 1;
129 }
130
131 const importDeclaration = module.imports[ subject.name ];
132
133 if ( !scope.contains( subject.name ) && importDeclaration ) {
134 const minDepth = importDeclaration.name === '*' ?
135 2 : // cannot do e.g. `namespace.foo = bar`
136 1; // cannot do e.g. `foo = bar`, but `foo.bar = bar` is fine
137
138 if ( depth < minDepth ) {
139 const err = new Error( `Illegal reassignment to import '${subject.name}'` );
140 err.file = module.id;
141 err.loc = getLocation( module.magicString.toString(), subject.start );
142 throw err;
143 }
144 }
145
146 isReassignment = !depth;
147 }
148
149 if ( isReference( node, parent ) ) {
150 // function declaration IDs are a special case – they're associated
151 // with the parent scope
152 const referenceScope = parent.type === 'FunctionDeclaration' && node === parent.id ?
153 scope.parent :
154 scope;
155
156 const reference = new Reference( node, referenceScope );
157 references.push( reference );
158
159 reference.isImmediatelyUsed = !readDepth;
160 reference.isReassignment = isReassignment;
161
162 this.skip(); // don't descend from `foo.bar.baz` into `foo.bar`
163 }
164 },
165 leave ( node, parent ) {
166 if ( node._scope ) scope = scope.parent;
167 if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth -= 1;
168 }
169 });
170 }
171
172 mark () {
173 if ( this.isIncluded ) return; // prevent infinite loops
174 this.isIncluded = true;
175
176 this.references.forEach( reference => {
177 if ( reference.declaration ) reference.declaration.use();
178 });
179 }
180
181 markSideEffect () {
182 if ( this.isIncluded ) return;
183
184 const statement = this;
185 let hasSideEffect = false;
186
187 walk( this.node, {
188 enter ( node, parent ) {
189 if ( /Function/.test( node.type ) && !isIife( node, parent ) ) return this.skip();
190
191 // If this is a top-level call expression, or an assignment to a global,
192 // this statement will need to be marked
193 if ( node.type === 'CallExpression' || node.type === 'NewExpression' ) {
194 hasSideEffect = true;
195 }
196
197 else if ( node.type in modifierNodes ) {
198 let subject = node[ modifierNodes[ node.type ] ];
199 while ( subject.type === 'MemberExpression' ) subject = subject.object;
200
201 const declaration = statement.module.trace( subject.name );
202
203 if ( !declaration || declaration.isExternal || declaration.statement.isIncluded ) {
204 hasSideEffect = true;
205 }
206 }
207
208 if ( hasSideEffect ) this.skip();
209 }
210 });
211
212 if ( hasSideEffect ) statement.mark();
213 return hasSideEffect;
214 }
215
216 source () {
217 return this.module.source.slice( this.start, this.end );
218 }
219
220 toString () {
221 return this.module.magicString.slice( this.start, this.end );
222 }
223}