OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library stub_core_library; | |
6 | |
7 import 'package:analyzer/analyzer.dart'; | |
8 import 'package:analyzer/src/generated/java_core.dart'; | |
9 import 'package:analyzer/src/generated/scanner.dart'; | |
10 import 'package:path/path.dart' as p; | |
11 | |
12 /// Returns the contents of a stub version of the library at [path]. | |
13 /// | |
14 /// A stub library has the same API as the original library, but none of the | |
15 /// implementation. Specifically, this guarantees that any code that worked with | |
16 /// the original library will be statically valid with the stubbed library, and | |
17 /// its only runtime errors will be [UnsupportedError]s. This means that | |
18 /// constants and const constructors are preserved. | |
19 /// | |
20 /// [importReplacements] is a map from import URIs to their replacements. It's | |
21 /// used so that mutliple interrelated libraries can refer to their stubbed | |
22 /// versions rather than the originals. | |
23 String stubFile(String path, [Map<String, String> importReplacements]) { | |
24 var visitor = new _StubVisitor(path, importReplacements); | |
25 parseDartFile(path).accept(visitor); | |
26 return visitor.toString(); | |
27 } | |
28 | |
29 /// Returns the contents of a stub version of the library parsed from [code]. | |
30 /// | |
31 /// If [code] contains `part` directives, they will be resolved relative to | |
32 /// [path]. The contents of the parted files will be stubbed and inlined. | |
33 String stubCode(String code, String path, | |
34 [Map<String, String> importReplacements]) { | |
35 var visitor = new _StubVisitor(path, importReplacements); | |
36 parseCompilationUnit(code, name: path).accept(visitor); | |
37 return visitor.toString(); | |
38 } | |
39 | |
40 /// An AST visitor that traverses the tree of the original library and writes | |
41 /// the stubbed version. | |
42 /// | |
43 /// In order to avoid complex tree-shaking logic, this takes a conservative | |
44 /// approach to removing private code. Private classes may still be extended by | |
45 /// public classes; private constants may be referenced by public constants; and | |
46 /// private static and top-level methods may be referenced by public constants | |
47 /// or by superclass constructor calls. All of these are preserved even though | |
48 /// most could theoretically be eliminated. | |
49 class _StubVisitor extends ToSourceVisitor { | |
50 /// The directory containing the library being visited. | |
51 final String _root; | |
52 | |
53 /// Which imports to replace. | |
54 final Map<String, String> _importReplacements; | |
55 | |
56 final PrintStringWriter _writer; | |
57 | |
58 // TODO(nweiz): Get rid of this when issue 19897 is fixed. | |
59 /// The current class declaration being visited. | |
60 /// | |
61 /// This is `null` if there is no current class declaration. | |
62 ClassDeclaration _class; | |
63 | |
64 _StubVisitor(String path, Map<String, String> importReplacements) | |
65 : this._(path, importReplacements, new PrintStringWriter()); | |
66 | |
67 _StubVisitor._(String path, Map<String, String> importReplacements, | |
68 PrintStringWriter writer) | |
69 : _root = p.dirname(path), | |
70 _importReplacements = importReplacements == null ? const {} : | |
71 importReplacements, | |
72 _writer = writer, | |
73 super(writer); | |
74 | |
75 String toString() => _writer.toString(); | |
76 | |
77 visitImportDirective(ImportDirective node) { | |
78 node = _modifyDirective(node); | |
79 if (node != null) super.visitImportDirective(node); | |
80 } | |
81 | |
82 visitExportDirective(ExportDirective node) { | |
83 node = _modifyDirective(node); | |
84 if (node != null) super.visitExportDirective(node); | |
85 } | |
86 | |
87 visitPartDirective(PartDirective node) { | |
88 // Inline parts directly in the output file. | |
89 var path = p.url.join(_root, p.fromUri(node.uri.stringValue)); | |
90 parseDartFile(path).accept(new _StubVisitor._(path, const {}, _writer)); | |
91 } | |
92 | |
93 visitPartOfDirective(PartOfDirective node) { | |
94 // Remove "part of", since parts are inlined. | |
95 } | |
96 | |
97 visitClassDeclaration(ClassDeclaration node) { | |
98 _class = _clone(node); | |
99 _class.nativeClause = null; | |
100 super.visitClassDeclaration(_class); | |
101 _class = null; | |
102 } | |
103 | |
104 visitConstructorDeclaration(ConstructorDeclaration node) { | |
105 node = _withoutExternal(node); | |
106 | |
107 // Remove field initializers and redirecting initializers but not superclass | |
108 // initializers. The code is ugly because NodeList doesn't support | |
109 // removeWhere. | |
110 var superclassInitializers = node.initializers.where((initializer) => | |
111 initializer is SuperConstructorInvocation).toList(); | |
112 node.initializers.clear(); | |
113 node.initializers.addAll(superclassInitializers); | |
114 | |
115 // Add a space because ToSourceVisitor doesn't and it makes testing easier. | |
116 _writer.print(" "); | |
117 super.visitConstructorDeclaration(node); | |
118 } | |
119 | |
120 visitSuperConstructorInvocation(SuperConstructorInvocation node) { | |
121 // If this is a const constructor, it should actually work, so don't screw | |
122 // with the superclass constructor. | |
123 if ((node.parent as ConstructorDeclaration).constKeyword != null) { | |
124 return super.visitSuperConstructorInvocation(node); | |
125 } | |
126 | |
127 _writer.print("super"); | |
128 _visitNodeWithPrefix(".", node.constructorName); | |
129 _writer.print("("); | |
130 | |
131 // If one stubbed class extends another, we don't want to run the original | |
132 // code for the superclass constructor call, and we do want an | |
133 // UnsupportedException that points to the subclass rather than the | |
134 // superclass. To do this, we null out all but the first superclass | |
135 // constructor parameter and replace the first parameter with a throw. | |
136 var positionalArguments = node.argumentList.arguments | |
137 .where((argument) => argument is! NamedExpression); | |
138 if (positionalArguments.isNotEmpty) { | |
139 _writer.print(_unsupported(_functionName(node))); | |
140 for (var i = 0; i < positionalArguments.length - 1; i++) { | |
141 _writer.print(", null"); | |
142 } | |
143 } | |
144 | |
145 _writer.print(")"); | |
146 } | |
147 | |
148 visitMethodDeclaration(MethodDeclaration node) { | |
149 // Private non-static methods aren't public and aren't accessible from | |
150 // constant expressions, so can be safely removed. | |
151 if (Identifier.isPrivateName(node.name.name) && !node.isStatic) return; | |
152 _writer.print(" "); | |
153 super.visitMethodDeclaration(_withoutExternal(node)); | |
154 } | |
155 | |
156 visitFunctionDeclaration(FunctionDeclaration node) { | |
157 super.visitFunctionDeclaration(_withoutExternal(node)); | |
158 } | |
159 | |
160 visitBlockFunctionBody(BlockFunctionBody node) => _emitFunctionBody(node); | |
161 | |
162 visitExpressionFunctionBody(ExpressionFunctionBody node) => | |
163 _emitFunctionBody(node); | |
164 | |
165 visitNativeFunctionBody(NativeFunctionBody node) => _emitFunctionBody(node); | |
166 | |
167 visitEmptyFunctionBody(FunctionBody node) { | |
168 // Preserve empty function bodies for abstract methods, since there's no | |
169 // reason not to. Note that "empty" here means "foo();" not "foo() {}". | |
170 var isAbstractMethod = node.parent is MethodDeclaration && | |
171 !(node.parent as MethodDeclaration).isStatic && _class != null && | |
172 _class.isAbstract; | |
173 | |
174 // Preserve empty function bodies for const constructors because we want | |
175 // them to continue to work. | |
176 var isConstConstructor = node.parent is ConstructorDeclaration && | |
177 (node.parent as ConstructorDeclaration).constKeyword != null; | |
178 | |
179 if (isAbstractMethod || isConstConstructor) { | |
180 super.visitEmptyFunctionBody(node); | |
181 _writer.print(" "); | |
182 } else { | |
183 _writer.print(" "); | |
184 _emitFunctionBody(node); | |
185 } | |
186 } | |
187 | |
188 visitFieldFormalParameter(FieldFormalParameter node) { | |
189 // Remove "this." because instance variables are replaced with getters and | |
190 // setters or just set to null. | |
191 _emitTokenWithSuffix(node.keyword, " "); | |
192 | |
193 // Make sure the parameter is still typed by grabbing the type from the | |
194 // associated instance variable. | |
195 var type = node.type; | |
196 if (type == null) { | |
197 var variable = _class.members | |
198 .where((member) => member is FieldDeclaration) | |
199 .expand((member) => member.fields.variables) | |
200 .firstWhere((variable) => variable.name.name == node.identifier.name, | |
201 orElse: () => null); | |
202 if (variable != null) type = variable.parent.type; | |
203 } | |
204 | |
205 _visitNodeWithSuffix(type, " "); | |
206 _visitNode(node.identifier); | |
207 _visitNode(node.parameters); | |
208 } | |
209 | |
210 visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { | |
211 node.variables.variables.forEach(_emitVariableDeclaration); | |
212 } | |
213 | |
214 visitFieldDeclaration(FieldDeclaration node) { | |
215 _writer.print(" "); | |
216 node.fields.variables.forEach(_emitVariableDeclaration); | |
217 } | |
218 | |
219 /// Modifies a directive to respect [importReplacements] and ignore hidden | |
220 /// core libraries. | |
221 /// | |
222 /// This can return `null`, indicating that the directive should not be | |
223 /// emitted. | |
224 UriBasedDirective _modifyDirective(UriBasedDirective node) { | |
225 // Ignore internal "dart:" libraries. | |
226 if (node.uri.stringValue.startsWith('dart:_')) return null; | |
227 | |
228 // Replace libraries in [importReplacements]. | |
229 if (_importReplacements.containsKey(node.uri.stringValue)) { | |
230 node = _clone(node); | |
231 var token = new StringToken(TokenType.STRING, | |
232 '"${_importReplacements[node.uri.stringValue]}"', 0); | |
233 node.uri = new SimpleStringLiteral(token, null); | |
234 } | |
235 | |
236 return node; | |
237 } | |
238 | |
239 /// Emits a variable declaration, either as a literal variable or as a getter | |
240 /// and maybe a setter that throw [UnsupportedError]s. | |
241 _emitVariableDeclaration(VariableDeclaration node) { | |
242 VariableDeclarationList parent = node.parent; | |
243 var isStatic = node.parent.parent is FieldDeclaration && | |
244 (node.parent.parent as FieldDeclaration).isStatic; | |
245 | |
246 // Preserve constants as-is. | |
247 if (node.isConst) { | |
248 if (isStatic) _writer.print("static "); | |
249 _writer.print("const "); | |
250 _visitNode(node); | |
251 _writer.print("; "); | |
252 return; | |
253 } | |
254 | |
255 // Ignore non-const private variables. | |
256 if (Identifier.isPrivateName(node.name.name)) return; | |
257 | |
258 // There's no need to throw errors for instance fields of classes that can't | |
259 // be constructed. | |
260 if (!isStatic && _class != null && !_inConstructableClass) { | |
261 _emitTokenWithSuffix(parent.keyword, " "); | |
262 _visitNodeWithSuffix(parent.type, " "); | |
263 _visitNode(node.name); | |
264 // Add an initializer to make sure that final variables are initialized. | |
265 if (node.isFinal) _writer.print(" = null; "); | |
266 return; | |
267 } | |
268 | |
269 var name = node.name.name; | |
270 if (_class != null) name = "${_class.name}.$name"; | |
271 | |
272 // Convert public variables into getters and setters that throw | |
273 // UnsupportedErrors. | |
274 if (isStatic) _writer.print("static "); | |
275 _visitNodeWithSuffix(parent.type, " "); | |
276 _writer.print("get "); | |
277 _visitNode(node.name); | |
278 _writer.print(" => ${_unsupported(name)}; "); | |
279 if (node.isFinal) return; | |
280 | |
281 if (isStatic) _writer.print("static "); | |
282 _writer.print("set "); | |
283 _visitNode(node.name); | |
284 _writer.print("("); | |
285 _visitNodeWithSuffix(parent.type, " "); | |
286 _writer.print("_) { ${_unsupported("$name=")}; } "); | |
287 } | |
288 | |
289 /// Emits a function body. | |
290 /// | |
291 /// This usually emits a body that throws an [UnsupportedError], but it can | |
292 /// emit an empty body as well. | |
293 void _emitFunctionBody(FunctionBody node) { | |
294 // There's no need to throw errors for instance methods of classes that | |
295 // can't be constructed. | |
296 var parent = node.parent; | |
297 if (parent is MethodDeclaration && !parent.isStatic && | |
298 !_inConstructableClass) { | |
299 _writer.print('{} '); | |
300 return; | |
301 } | |
302 | |
303 _writer.print('{ ${_unsupported(_functionName(node))}; } '); | |
304 } | |
305 | |
306 // Returns a human-readable name for the function containing [node]. | |
307 String _functionName(AstNode node) { | |
308 // Come up with a nice name for the error message so users can tell exactly | |
309 // what unsupported method they're calling. | |
310 var function = node.getAncestor((ancestor) => | |
311 ancestor is FunctionDeclaration || ancestor is MethodDeclaration); | |
312 if (function != null) { | |
313 var name = function.name.name; | |
314 if (function.isSetter) { | |
315 name = "$name="; | |
316 } else if (!function.isGetter && | |
317 !(function is MethodDeclaration && function.isOperator)) { | |
318 name = "$name()"; | |
319 } | |
320 if (_class != null) name = "${_class.name}.$name"; | |
321 return name; | |
322 } | |
323 | |
324 var constructor = node.getAncestor((ancestor) => | |
325 ancestor is ConstructorDeclaration); | |
326 if (constructor == null) return "This function"; | |
327 | |
328 var name = "new ${constructor.returnType.name}"; | |
329 if (constructor.name != null) name = "$name.${constructor.name}"; | |
330 return "$name()"; | |
331 } | |
332 | |
333 /// Returns a deep copy of [node]. | |
334 AstNode _clone(AstNode node) => node.accept(new AstCloner()); | |
335 | |
336 /// Returns a deep copy of [node] without the "external" keyword. | |
337 AstNode _withoutExternal(node) { | |
338 var clone = node.accept(new AstCloner()); | |
339 clone.externalKeyword = null; | |
340 return clone; | |
341 } | |
342 | |
343 /// Visits [node] if it's non-`null`. | |
344 void _visitNode(AstNode node) { | |
345 if (node != null) node.accept(this); | |
346 } | |
347 | |
348 /// Visits [node] then emits [suffix] if [node] isn't `null`. | |
349 void _visitNodeWithSuffix(AstNode node, String suffix) { | |
350 if (node == null) return; | |
351 node.accept(this); | |
352 _writer.print(suffix); | |
353 } | |
354 | |
355 /// Emits [prefix] then visits [node] if [node] isn't `null`. | |
356 void _visitNodeWithPrefix(String prefix, AstNode node) { | |
357 if (node == null) return; | |
358 _writer.print(prefix); | |
359 node.accept(this); | |
360 } | |
361 | |
362 /// Emits [token] followed by [suffix] if [token] isn't `null`. | |
363 void _emitTokenWithSuffix(Token token, String suffix) { | |
364 if (token == null) return; | |
365 _writer.print(token.lexeme); | |
366 _writer.print(suffix); | |
367 } | |
368 | |
369 /// Returns an expression that throws an [UnsupportedError] explaining that | |
370 /// [name] isn't supported. | |
371 String _unsupported(String name) => 'throw new UnsupportedError("$name is ' | |
372 'unsupported on this platform.")'; | |
373 | |
374 /// Returns whether or not the visitor is currently visiting a class that can | |
375 /// be constructed without error after it's stubbed. | |
376 /// | |
377 /// There are two cases where a class will be constructable once it's been | |
378 /// stubbed. First, a class with a const constructor will be preserved, since | |
379 /// making the const constructor fail would statically break code. Second, a | |
380 /// class with a default constructor is preserved since adding a constructor | |
381 /// that throws an error could statically break uses of the class as a mixin. | |
382 bool get _inConstructableClass { | |
383 if (_class == null) return false; | |
384 | |
385 var constructors = _class.members.where((member) => | |
386 member is ConstructorDeclaration); | |
387 if (constructors.isEmpty) return true; | |
388 | |
389 return constructors.any((constructor) => constructor.constKeyword != null); | |
390 } | |
391 } | |
OLD | NEW |