Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1188)

Side by Side Diff: packages/intl/lib/extract_messages.dart

Issue 2989763002: Update charted to 0.4.8 and roll (Closed)
Patch Set: Removed Cutch from list of reviewers Created 3 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « packages/intl/lib/date_time_patterns.dart ('k') | packages/intl/lib/generate_localized.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « packages/intl/lib/date_time_patterns.dart ('k') | packages/intl/lib/generate_localized.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698