OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, 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 /** | |
6 * This is for use in extracting messages from a Dart program | |
7 * using the Intl.message() mechanism and writing them to a file for | |
8 * translation. This provides only the stub of a mechanism, because it | |
9 * doesn't define how the file should be written. It provides an | |
10 * [IntlMessage] class that holds the extracted data and [parseString] | |
11 * and [parseFile] methods which | |
12 * can extract messages that conform to the expected pattern: | |
13 * (parameters) => Intl.message("Message $parameters", desc: ...); | |
14 * It uses the analyzer package to do the parsing, so may | |
15 * break if there are changes to the API that it provides. | |
16 * An example can be found in test/message_extraction/extract_to_json.dart | |
17 * | |
18 * Note that this does not understand how to follow part directives, so it | |
19 * has to explicitly be given all the files that it needs. A typical use case | |
20 * is to run it on all .dart files in a directory. | |
21 */ | |
22 library extract_messages; | |
23 | |
24 import 'dart:io'; | |
25 | |
26 import 'package:analyzer/analyzer.dart'; | |
27 import 'package:intl/src/intl_message.dart'; | |
28 | |
29 /** | |
30 * If this is true, print warnings for skipped messages. Otherwise, warnings | |
31 * are suppressed. | |
32 */ | |
33 bool suppressWarnings = false; | |
34 | |
35 /** | |
36 * If this is true, then treat all warnings as errors. | |
37 */ | |
38 bool warningsAreErrors = false; | |
39 | |
40 /** | |
41 * This accumulates a list of all warnings/errors we have found. These are | |
42 * saved as strings right now, so all that can really be done is print and | |
43 * count them. | |
44 */ | |
45 List<String> warnings = []; | |
46 | |
47 /** Were there any warnings or errors in extracting messages. */ | |
48 bool get hasWarnings => warnings.isNotEmpty; | |
49 | |
50 /** Are plural and gender expressions required to be at the top level | |
51 * of an expression, or are they allowed to be embedded in string literals. | |
52 * | |
53 * For example, the following expression | |
54 * 'There are ${Intl.plural(...)} items'. | |
55 * is legal if [allowEmbeddedPluralsAndGenders] is true, but illegal | |
56 * if [allowEmbeddedPluralsAndGenders] is false. | |
57 */ | |
58 bool allowEmbeddedPluralsAndGenders = true; | |
59 | |
60 /** | |
61 * Parse the source of the Dart program file [file] and return a Map from | |
62 * message names to [IntlMessage] instances. | |
63 */ | |
64 Map<String, MainMessage> parseFile(File file) { | |
65 try { | |
66 _root = parseDartFile(file.path); | |
67 } on AnalyzerErrorGroup catch (e) { | |
68 print("Error in parsing ${file.path}, no messages extracted."); | |
69 print(" $e"); | |
70 return {}; | |
71 } | |
72 _origin = file.path; | |
73 var visitor = new MessageFindingVisitor(); | |
74 _root.accept(visitor); | |
75 return visitor.messages; | |
76 } | |
77 | |
78 /** | |
79 * The root of the compilation unit, and the first node we visit. We hold | |
80 * on to this for error reporting, as it can give us line numbers of other | |
81 * nodes. | |
82 */ | |
83 CompilationUnit _root; | |
84 | |
85 /** | |
86 * An arbitrary string describing where the source code came from. Most | |
87 * obviously, this could be a file path. We use this when reporting | |
88 * invalid messages. | |
89 */ | |
90 String _origin; | |
91 | |
92 String _reportErrorLocation(AstNode node) { | |
93 var result = new StringBuffer(); | |
94 if (_origin != null) result.write(" from $_origin"); | |
95 var info = _root.lineInfo; | |
96 if (info != null) { | |
97 var line = info.getLocation(node.offset); | |
98 result.write(" line: ${line.lineNumber}, column: ${line.columnNumber}"); | |
99 } | |
100 return result.toString(); | |
101 } | |
102 | |
103 /** | |
104 * This visits the program source nodes looking for Intl.message uses | |
105 * that conform to its pattern and then creating the corresponding | |
106 * IntlMessage objects. We have to find both the enclosing function, and | |
107 * the Intl.message invocation. | |
108 */ | |
109 class MessageFindingVisitor extends GeneralizingAstVisitor { | |
110 MessageFindingVisitor(); | |
111 | |
112 /** | |
113 * Accumulates the messages we have found, keyed by name. | |
114 */ | |
115 final Map<String, MainMessage> messages = new Map<String, MainMessage>(); | |
116 | |
117 /** | |
118 * We keep track of the data from the last MethodDeclaration, | |
119 * FunctionDeclaration or FunctionExpression that we saw on the way down, | |
120 * as that will be the nearest parent of the Intl.message invocation. | |
121 */ | |
122 FormalParameterList parameters; | |
123 String name; | |
124 | |
125 /** Return true if [node] matches the pattern we expect for Intl.message() */ | |
126 bool looksLikeIntlMessage(MethodInvocation node) { | |
127 const validNames = const ["message", "plural", "gender", "select"]; | |
128 if (!validNames.contains(node.methodName.name)) return false; | |
129 if (!(node.target is SimpleIdentifier)) return false; | |
130 SimpleIdentifier target = node.target; | |
131 return target.token.toString() == "Intl"; | |
132 } | |
133 | |
134 Message _expectedInstance(String type) { | |
135 switch (type) { | |
136 case 'message': | |
137 return new MainMessage(); | |
138 case 'plural': | |
139 return new Plural(); | |
140 case 'gender': | |
141 return new Gender(); | |
142 case 'select': | |
143 return new Select(); | |
144 default: | |
145 return null; | |
146 } | |
147 } | |
148 | |
149 /** | |
150 * Returns a String describing why the node is invalid, or null if no | |
151 * reason is found, so it's presumed valid. | |
152 */ | |
153 String checkValidity(MethodInvocation node) { | |
154 // The containing function cannot have named parameters. | |
155 if (parameters.parameters.any((each) => each.kind == ParameterKind.NAMED)) { | |
156 return "Named parameters on message functions are not supported."; | |
157 } | |
158 var arguments = node.argumentList.arguments; | |
159 var instance = _expectedInstance(node.methodName.name); | |
160 return instance.checkValidity(node, arguments, name, parameters); | |
161 } | |
162 | |
163 /** | |
164 * Record the parameters of the function or method declaration we last | |
165 * encountered before seeing the Intl.message call. | |
166 */ | |
167 void visitMethodDeclaration(MethodDeclaration node) { | |
168 parameters = node.parameters; | |
169 if (parameters == null) { | |
170 parameters = new FormalParameterList(null, [], null, null, null); | |
171 } | |
172 name = node.name.name; | |
173 super.visitMethodDeclaration(node); | |
174 } | |
175 | |
176 /** | |
177 * Record the parameters of the function or method declaration we last | |
178 * encountered before seeing the Intl.message call. | |
179 */ | |
180 void visitFunctionDeclaration(FunctionDeclaration node) { | |
181 parameters = node.functionExpression.parameters; | |
182 if (parameters == null) { | |
183 parameters = new FormalParameterList(null, [], null, null, null); | |
184 } | |
185 name = node.name.name; | |
186 super.visitFunctionDeclaration(node); | |
187 } | |
188 | |
189 /** | |
190 * Examine method invocations to see if they look like calls to Intl.message. | |
191 * If we've found one, stop recursing. This is important because we can have | |
192 * Intl.message(...Intl.plural...) and we don't want to treat the inner | |
193 * plural as if it was an outermost message. | |
194 */ | |
195 void visitMethodInvocation(MethodInvocation node) { | |
196 if (!addIntlMessage(node)) { | |
197 super.visitMethodInvocation(node); | |
198 } | |
199 } | |
200 | |
201 /** | |
202 * Check that the node looks like an Intl.message invocation, and create | |
203 * the [IntlMessage] object from it and store it in [messages]. Return true | |
204 * if we successfully extracted a message and should stop looking. Return | |
205 * false if we didn't, so should continue recursing. | |
206 */ | |
207 bool addIntlMessage(MethodInvocation node) { | |
208 if (!looksLikeIntlMessage(node)) return false; | |
209 var reason = checkValidity(node); | |
210 if (reason != null) { | |
211 if (!suppressWarnings) { | |
212 var err = new StringBuffer() | |
213 ..write("Skipping invalid Intl.message invocation\n <$node>\n") | |
214 ..writeAll([" reason: $reason\n", _reportErrorLocation(node)]); | |
215 warnings.add(err.toString()); | |
216 print(err); | |
217 } | |
218 // We found one, but it's not valid. Stop recursing. | |
219 return true; | |
220 } | |
221 var message; | |
222 if (node.methodName.name == "message") { | |
223 message = messageFromIntlMessageCall(node); | |
224 } else { | |
225 message = messageFromDirectPluralOrGenderCall(node); | |
226 } | |
227 if (message != null) messages[message.name] = message; | |
228 return true; | |
229 } | |
230 | |
231 /** | |
232 * Create a MainMessage from [node] using the name and | |
233 * parameters of the last function/method declaration we encountered, | |
234 * and the values we get by calling [extract]. We set those values | |
235 * by calling [setAttribute]. This is the common parts between | |
236 * [messageFromIntlMessageCall] and [messageFromDirectPluralOrGenderCall]. | |
237 */ | |
238 MainMessage _messageFromNode( | |
239 MethodInvocation node, Function extract, Function setAttribute) { | |
240 var message = new MainMessage(); | |
241 message.name = name; | |
242 message.arguments = | |
243 parameters.parameters.map((x) => x.identifier.name).toList(); | |
244 var arguments = node.argumentList.arguments; | |
245 var extractionResult = extract(message, arguments); | |
246 if (extractionResult == null) return null; | |
247 | |
248 for (var namedArgument in arguments.where((x) => x is NamedExpression)) { | |
249 var name = namedArgument.name.label.name; | |
250 var exp = namedArgument.expression; | |
251 var evaluator = new ConstantEvaluator(); | |
252 var basicValue = exp.accept(evaluator); | |
253 var value = basicValue == ConstantEvaluator.NOT_A_CONSTANT | |
254 ? exp.toString() | |
255 : basicValue; | |
256 setAttribute(message, name, value); | |
257 } | |
258 return message; | |
259 } | |
260 | |
261 /** | |
262 * Create a MainMessage from [node] using the name and | |
263 * parameters of the last function/method declaration we encountered | |
264 * and the parameters to the Intl.message call. | |
265 */ | |
266 MainMessage messageFromIntlMessageCall(MethodInvocation node) { | |
267 MainMessage extractFromIntlCall(MainMessage message, List arguments) { | |
268 try { | |
269 var interpolation = new InterpolationVisitor(message); | |
270 arguments.first.accept(interpolation); | |
271 if (interpolation.pieces.any((x) => x is Plural || x is Gender) && | |
272 !allowEmbeddedPluralsAndGenders) { | |
273 if (interpolation.pieces.any((x) => x is String && x.isNotEmpty)) { | |
274 throw new IntlMessageExtractionException( | |
275 "Plural and gender expressions must be at the top level, " | |
276 "they cannot be embedded in larger string literals.\n" | |
277 "Error at $node"); | |
278 } | |
279 } | |
280 message.messagePieces.addAll(interpolation.pieces); | |
281 } on IntlMessageExtractionException catch (e) { | |
282 message = null; | |
283 var err = new StringBuffer() | |
284 ..writeAll(["Error ", e, "\nProcessing <", node, ">\n"]) | |
285 ..write(_reportErrorLocation(node)); | |
286 print(err); | |
287 warnings.add(err.toString()); | |
288 } | |
289 return message; // Because we may have set it to null on an error. | |
290 } | |
291 | |
292 void setValue(MainMessage message, String fieldName, Object fieldValue) { | |
293 message[fieldName] = fieldValue; | |
294 } | |
295 | |
296 return _messageFromNode(node, extractFromIntlCall, setValue); | |
297 } | |
298 | |
299 /** | |
300 * Create a MainMessage from [node] using the name and | |
301 * parameters of the last function/method declaration we encountered | |
302 * and the parameters to the Intl.plural or Intl.gender call. | |
303 */ | |
304 MainMessage messageFromDirectPluralOrGenderCall(MethodInvocation node) { | |
305 MainMessage extractFromPluralOrGender(MainMessage message, _) { | |
306 var visitor = new PluralAndGenderVisitor(message.messagePieces, message); | |
307 node.accept(visitor); | |
308 return message; | |
309 } | |
310 | |
311 void setAttribute(MainMessage msg, String fieldName, String fieldValue) { | |
312 if (msg.attributeNames.contains(fieldName)) { | |
313 msg[fieldName] = fieldValue; | |
314 } | |
315 } | |
316 return _messageFromNode(node, extractFromPluralOrGender, setAttribute); | |
317 } | |
318 } | |
319 | |
320 /** | |
321 * Given an interpolation, find all of its chunks, validate that they are only | |
322 * simple variable substitutions or else Intl.plural/gender calls, | |
323 * and keep track of the pieces of text so that other parts | |
324 * of the program can deal with the simple string sections and the generated | |
325 * parts separately. Note that this is a SimpleAstVisitor, so it only | |
326 * traverses one level of children rather than automatically recursing. If we | |
327 * find a plural or gender, which requires recursion, we do it with a separate | |
328 * special-purpose visitor. | |
329 */ | |
330 class InterpolationVisitor extends SimpleAstVisitor { | |
331 final Message message; | |
332 | |
333 InterpolationVisitor(this.message); | |
334 | |
335 List pieces = []; | |
336 String get extractedMessage => pieces.join(); | |
337 | |
338 void visitAdjacentStrings(AdjacentStrings node) { | |
339 node.visitChildren(this); | |
340 super.visitAdjacentStrings(node); | |
341 } | |
342 | |
343 void visitStringInterpolation(StringInterpolation node) { | |
344 node.visitChildren(this); | |
345 super.visitStringInterpolation(node); | |
346 } | |
347 | |
348 void visitSimpleStringLiteral(SimpleStringLiteral node) { | |
349 pieces.add(node.value); | |
350 super.visitSimpleStringLiteral(node); | |
351 } | |
352 | |
353 void visitInterpolationString(InterpolationString node) { | |
354 pieces.add(node.value); | |
355 super.visitInterpolationString(node); | |
356 } | |
357 | |
358 void visitInterpolationExpression(InterpolationExpression node) { | |
359 if (node.expression is SimpleIdentifier) { | |
360 return handleSimpleInterpolation(node); | |
361 } else { | |
362 return lookForPluralOrGender(node); | |
363 } | |
364 // Note that we never end up calling super. | |
365 } | |
366 | |
367 lookForPluralOrGender(InterpolationExpression node) { | |
368 var visitor = new PluralAndGenderVisitor(pieces, message); | |
369 node.accept(visitor); | |
370 if (!visitor.foundPluralOrGender) { | |
371 throw new IntlMessageExtractionException( | |
372 "Only simple identifiers and Intl.plural/gender/select expressions " | |
373 "are allowed in message " | |
374 "interpolation expressions.\nError at $node"); | |
375 } | |
376 } | |
377 | |
378 void handleSimpleInterpolation(InterpolationExpression node) { | |
379 var index = arguments.indexOf(node.expression.toString()); | |
380 if (index == -1) { | |
381 throw new IntlMessageExtractionException( | |
382 "Cannot find argument ${node.expression}"); | |
383 } | |
384 pieces.add(index); | |
385 } | |
386 | |
387 List get arguments => message.arguments; | |
388 } | |
389 | |
390 /** | |
391 * A visitor to extract information from Intl.plural/gender sends. Note that | |
392 * this is a SimpleAstVisitor, so it doesn't automatically recurse. So this | |
393 * needs to be called where we expect a plural or gender immediately below. | |
394 */ | |
395 class PluralAndGenderVisitor extends SimpleAstVisitor { | |
396 /** | |
397 * A plural or gender always exists in the context of a parent message, | |
398 * which could in turn also be a plural or gender. | |
399 */ | |
400 final ComplexMessage parent; | |
401 | |
402 /** | |
403 * The pieces of the message. We are given an initial version of this | |
404 * from our parent and we add to it as we find additional information. | |
405 */ | |
406 List pieces; | |
407 | |
408 /** This will be set to true if we find a plural or gender. */ | |
409 bool foundPluralOrGender = false; | |
410 | |
411 PluralAndGenderVisitor(this.pieces, this.parent) : super(); | |
412 | |
413 visitInterpolationExpression(InterpolationExpression node) { | |
414 // TODO(alanknight): Provide better errors for malformed expressions. | |
415 if (!looksLikePluralOrGender(node.expression)) return; | |
416 var reason = checkValidity(node.expression); | |
417 if (reason != null) throw reason; | |
418 var message = messageFromMethodInvocation(node.expression); | |
419 foundPluralOrGender = true; | |
420 pieces.add(message); | |
421 super.visitInterpolationExpression(node); | |
422 } | |
423 | |
424 visitMethodInvocation(MethodInvocation node) { | |
425 pieces.add(messageFromMethodInvocation(node)); | |
426 super.visitMethodInvocation(node); | |
427 } | |
428 | |
429 /** Return true if [node] matches the pattern for plural or gender message.*/ | |
430 bool looksLikePluralOrGender(MethodInvocation node) { | |
431 if (!["plural", "gender", "select"].contains(node.methodName.name)) { | |
432 return false; | |
433 } | |
434 if (!(node.target is SimpleIdentifier)) return false; | |
435 SimpleIdentifier target = node.target; | |
436 return target.token.toString() == "Intl"; | |
437 } | |
438 | |
439 /** | |
440 * Returns a String describing why the node is invalid, or null if no | |
441 * reason is found, so it's presumed valid. | |
442 */ | |
443 String checkValidity(MethodInvocation node) { | |
444 // TODO(alanknight): Add reasonable validity checks. | |
445 return null; | |
446 } | |
447 | |
448 /** | |
449 * Create a MainMessage from [node] using the name and | |
450 * parameters of the last function/method declaration we encountered | |
451 * and the parameters to the Intl.message call. | |
452 */ | |
453 Message messageFromMethodInvocation(MethodInvocation node) { | |
454 var message; | |
455 switch (node.methodName.name) { | |
456 case "gender": | |
457 message = new Gender(); | |
458 break; | |
459 case "plural": | |
460 message = new Plural(); | |
461 break; | |
462 case "select": | |
463 message = new Select(); | |
464 break; | |
465 default: | |
466 throw new IntlMessageExtractionException( | |
467 "Invalid plural/gender/select message"); | |
468 } | |
469 message.parent = parent; | |
470 | |
471 var arguments = message.argumentsOfInterestFor(node); | |
472 arguments.forEach((key, value) { | |
473 try { | |
474 var interpolation = new InterpolationVisitor(message); | |
475 value.accept(interpolation); | |
476 message[key] = interpolation.pieces; | |
477 } on IntlMessageExtractionException catch (e) { | |
478 message = null; | |
479 var err = new StringBuffer() | |
480 ..writeAll(["Error ", e, "\nProcessing <", node, ">"]) | |
481 ..write(_reportErrorLocation(node)); | |
482 print(err); | |
483 warnings.add(err.toString()); | |
484 } | |
485 }); | |
486 var mainArg = node.argumentList.arguments | |
487 .firstWhere((each) => each is! NamedExpression); | |
488 if (mainArg is SimpleStringLiteral) { | |
489 message.mainArgument = mainArg.toString(); | |
490 } else { | |
491 message.mainArgument = mainArg.name; | |
492 } | |
493 return message; | |
494 } | |
495 } | |
496 | |
497 /** | |
498 * Exception thrown when we cannot process a message properly. | |
499 */ | |
500 class IntlMessageExtractionException implements Exception { | |
501 /** | |
502 * A message describing the error. | |
503 */ | |
504 final String message; | |
505 | |
506 /** | |
507 * Creates a new exception with an optional error [message]. | |
508 */ | |
509 const IntlMessageExtractionException([this.message = ""]); | |
510 | |
511 String toString() => "IntlMessageExtractionException: $message"; | |
512 } | |
OLD | NEW |