| 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 |