| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * This is for use in extracting messages from a Dart program | 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 | 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 | 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 | 9 * doesn't define how the file should be written. It provides an |
| 10 * [IntlMessage] class that holds the extracted data and [parseString] | 10 * [IntlMessage] class that holds the extracted data and [parseString] |
| (...skipping 18 matching lines...) Expand all Loading... |
| 29 /** | 29 /** |
| 30 * If this is true, print warnings for skipped messages. Otherwise, warnings | 30 * If this is true, print warnings for skipped messages. Otherwise, warnings |
| 31 * are suppressed. | 31 * are suppressed. |
| 32 */ | 32 */ |
| 33 bool suppressWarnings = false; | 33 bool suppressWarnings = false; |
| 34 | 34 |
| 35 /** | 35 /** |
| 36 * Parse the source of the Dart program file [file] and return a Map from | 36 * Parse the source of the Dart program file [file] and return a Map from |
| 37 * message names to [IntlMessage] instances. | 37 * message names to [IntlMessage] instances. |
| 38 */ | 38 */ |
| 39 Map<String, IntlMessage> parseFile(File file) { | 39 Map<String, MainMessage> parseFile(File file) { |
| 40 var unit = parseDartFile(file.path); | 40 _root = parseDartFile(file.path); |
| 41 var visitor = new MessageFindingVisitor(unit, file.path); | 41 _origin = file.path; |
| 42 unit.accept(visitor); | 42 var visitor = new MessageFindingVisitor(); |
| 43 _root.accept(visitor); |
| 43 return visitor.messages; | 44 return visitor.messages; |
| 44 } | 45 } |
| 45 | 46 |
| 46 /** | 47 /** |
| 48 * The root of the compilation unit, and the first node we visit. We hold |
| 49 * on to this for error reporting, as it can give us line numbers of other |
| 50 * nodes. |
| 51 */ |
| 52 CompilationUnit _root; |
| 53 |
| 54 /** |
| 55 * An arbitrary string describing where the source code came from. Most |
| 56 * obviously, this could be a file path. We use this when reporting |
| 57 * invalid messages. |
| 58 */ |
| 59 String _origin; |
| 60 |
| 61 void _reportErrorLocation(ASTNode node) { |
| 62 if (_origin != null) print(" from $_origin"); |
| 63 var info = _root.lineInfo; |
| 64 if (info != null) { |
| 65 var line = info.getLocation(node.offset); |
| 66 print(" line: ${line.lineNumber}, column: ${line.columnNumber}"); |
| 67 } |
| 68 } |
| 69 |
| 70 /** |
| 47 * This visits the program source nodes looking for Intl.message uses | 71 * This visits the program source nodes looking for Intl.message uses |
| 48 * that conform to its pattern and then finding the | 72 * that conform to its pattern and then creating the corresponding |
| 73 * IntlMessage objects. We have to find both the enclosing function, and |
| 74 * the Intl.message invocation. |
| 49 */ | 75 */ |
| 50 class MessageFindingVisitor extends GeneralizingASTVisitor { | 76 class MessageFindingVisitor extends GeneralizingASTVisitor { |
| 51 | 77 |
| 52 /** | 78 MessageFindingVisitor(); |
| 53 * The root of the compilation unit, and the first node we visit. We hold | |
| 54 * on to this for error reporting, as it can give us line numbers of other | |
| 55 * nodes. | |
| 56 */ | |
| 57 final CompilationUnit root; | |
| 58 | 79 |
| 59 /** | 80 /** |
| 60 * An arbitrary string describing where the source code came from. Most | 81 * Accumulates the messages we have found, keyed by name. |
| 61 * obviously, this could be a file path. We use this when reporting | |
| 62 * invalid messages. | |
| 63 */ | 82 */ |
| 64 final String origin; | 83 final Map<String, MainMessage> messages = new Map<String, MainMessage>(); |
| 65 | |
| 66 MessageFindingVisitor(this.root, this.origin); | |
| 67 | |
| 68 /** | |
| 69 * Accumulates the messages we have found. | |
| 70 */ | |
| 71 final Map<String, IntlMessage> messages = new Map<String, IntlMessage>(); | |
| 72 | 84 |
| 73 /** | 85 /** |
| 74 * We keep track of the data from the last MethodDeclaration, | 86 * We keep track of the data from the last MethodDeclaration, |
| 75 * FunctionDeclaration or FunctionExpression that we saw on the way down, | 87 * FunctionDeclaration or FunctionExpression that we saw on the way down, |
| 76 * as that will be the nearest parent of the Intl.message invocation. | 88 * as that will be the nearest parent of the Intl.message invocation. |
| 77 */ | 89 */ |
| 78 FormalParameterList parameters; | 90 FormalParameterList parameters; |
| 79 String name; | 91 String name; |
| 80 | 92 |
| 81 /** Return true if [node] matches the pattern we expect for Intl.message() */ | 93 /** Return true if [node] matches the pattern we expect for Intl.message() */ |
| (...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 164 /** | 176 /** |
| 165 * Check that the node looks like an Intl.message invocation, and create | 177 * Check that the node looks like an Intl.message invocation, and create |
| 166 * the [IntlMessage] object from it and store it in [messages]. | 178 * the [IntlMessage] object from it and store it in [messages]. |
| 167 */ | 179 */ |
| 168 void addIntlMessage(MethodInvocation node) { | 180 void addIntlMessage(MethodInvocation node) { |
| 169 if (!looksLikeIntlMessage(node)) return; | 181 if (!looksLikeIntlMessage(node)) return; |
| 170 var reason = checkValidity(node); | 182 var reason = checkValidity(node); |
| 171 if (reason != null && !suppressWarnings) { | 183 if (reason != null && !suppressWarnings) { |
| 172 print("Skipping invalid Intl.message invocation\n <$node>"); | 184 print("Skipping invalid Intl.message invocation\n <$node>"); |
| 173 print(" reason: $reason"); | 185 print(" reason: $reason"); |
| 174 reportErrorLocation(node); | 186 _reportErrorLocation(node); |
| 175 return; | 187 return; |
| 176 } | 188 } |
| 177 var message = messageFromMethodInvocation(node); | 189 var message = messageFromMethodInvocation(node); |
| 178 if (message != null) messages[message.name] = message; | 190 if (message != null) messages[message.name] = message; |
| 179 } | 191 } |
| 180 | 192 |
| 181 /** | 193 /** |
| 182 * Create an IntlMessage from [node] using the name and | 194 * Create an IntlMessage from [node] using the name and |
| 183 * parameters of the last function/method declaration we encountered | 195 * parameters of the last function/method declaration we encountered |
| 184 * and the parameters to the Intl.message call. | 196 * and the parameters to the Intl.message call. |
| 185 */ | 197 */ |
| 186 IntlMessage messageFromMethodInvocation(MethodInvocation node) { | 198 MainMessage messageFromMethodInvocation(MethodInvocation node) { |
| 187 var message = new IntlMessage(); | 199 var message = new MainMessage(); |
| 188 message.name = name; | 200 message.name = name; |
| 189 message.arguments = parameters.parameters.elements.map( | 201 message.arguments = parameters.parameters.elements.map( |
| 190 (x) => x.identifier.name).toList(); | 202 (x) => x.identifier.name).toList(); |
| 203 var arguments = node.argumentList.arguments.elements; |
| 191 try { | 204 try { |
| 192 node.accept(new MessageVisitor(message)); | 205 var interpolation = new InterpolationVisitor(message); |
| 206 arguments.first.accept(interpolation); |
| 207 message.messagePieces.addAll(interpolation.pieces); |
| 193 } on IntlMessageExtractionException catch (e) { | 208 } on IntlMessageExtractionException catch (e) { |
| 194 message = null; | 209 message = null; |
| 195 print("Error $e"); | 210 print("Error $e"); |
| 196 print("Processing <$node>"); | 211 print("Processing <$node>"); |
| 197 reportErrorLocation(node); | 212 _reportErrorLocation(node); |
| 213 } |
| 214 for (NamedExpression namedArgument in arguments.skip(1)) { |
| 215 var name = namedArgument.name.label.name; |
| 216 var exp = namedArgument.expression; |
| 217 var string = exp is SimpleStringLiteral ? exp.value : exp.toString(); |
| 218 message[name] = string; |
| 198 } | 219 } |
| 199 return message; | 220 return message; |
| 200 } | 221 } |
| 201 | |
| 202 void reportErrorLocation(ASTNode node) { | |
| 203 if (origin != null) print(" from $origin"); | |
| 204 var info = root.lineInfo; | |
| 205 if (info != null) { | |
| 206 var line = info.getLocation(node.offset); | |
| 207 print(" line: ${line.lineNumber}, column: ${line.columnNumber}"); | |
| 208 } | |
| 209 } | |
| 210 } | |
| 211 | |
| 212 /** | |
| 213 * Given a node that looks like an invocation of Intl.message, extract out | |
| 214 * the message and the parameters and store them in [target]. | |
| 215 */ | |
| 216 class MessageVisitor extends GeneralizingASTVisitor { | |
| 217 IntlMessage target; | |
| 218 | |
| 219 MessageVisitor(IntlMessage this.target); | |
| 220 | |
| 221 /** | |
| 222 * Extract out the message string. If it's an interpolation, turn it into | |
| 223 * a single string with interpolation characters. | |
| 224 */ | |
| 225 void visitArgumentList(ArgumentList node) { | |
| 226 var interpolation = new InterpolationVisitor(target); | |
| 227 node.arguments.elements.first.accept(interpolation); | |
| 228 target.messagePieces = interpolation.pieces; | |
| 229 super.visitArgumentList(node); | |
| 230 } | |
| 231 | |
| 232 /** | |
| 233 * Find the values of all the named arguments, remove quotes, and save them | |
| 234 * into [target]. | |
| 235 */ | |
| 236 void visitNamedExpression(NamedExpression node) { | |
| 237 var name = node.name.label.name; | |
| 238 var exp = node.expression; | |
| 239 var string = exp is SimpleStringLiteral ? exp.value : exp.toString(); | |
| 240 target[name] = string; | |
| 241 super.visitNamedExpression(node); | |
| 242 } | |
| 243 } | 222 } |
| 244 | 223 |
| 245 /** | 224 /** |
| 246 * Given an interpolation, find all of its chunks, validate that they are only | 225 * Given an interpolation, find all of its chunks, validate that they are only |
| 247 * simple interpolations, and keep track of the chunks so that other parts | 226 * simple variable substitutions or else Intl.plural/gender calls, |
| 248 * of the program can deal with the interpolations and the simple string | 227 * and keep track of the pieces of text so that other parts |
| 249 * sections separately. | 228 * of the program can deal with the simple string sections and the generated |
| 229 * parts separately. Note that this is a SimpleASTVisitor, so it only |
| 230 * traverses one level of children rather than automatically recursing. If we |
| 231 * find a plural or gender, which requires recursion, we do it with a separate |
| 232 * special-purpose visitor. |
| 250 */ | 233 */ |
| 251 class InterpolationVisitor extends GeneralizingASTVisitor { | 234 class InterpolationVisitor extends SimpleASTVisitor { |
| 252 IntlMessage message; | 235 Message message; |
| 253 | 236 |
| 254 InterpolationVisitor(this.message); | 237 InterpolationVisitor(this.message); |
| 255 | 238 |
| 256 List pieces = []; | 239 List pieces = []; |
| 257 String get extractedMessage => pieces.join(); | 240 String get extractedMessage => pieces.join(); |
| 258 | 241 |
| 242 void visitAdjacentStrings(AdjacentStrings node) { |
| 243 node.visitChildren(this); |
| 244 super.visitAdjacentStrings(node); |
| 245 } |
| 246 |
| 247 void visitStringInterpolation(StringInterpolation node) { |
| 248 node.visitChildren(this); |
| 249 super.visitStringInterpolation(node); |
| 250 } |
| 251 |
| 259 void visitSimpleStringLiteral(SimpleStringLiteral node) { | 252 void visitSimpleStringLiteral(SimpleStringLiteral node) { |
| 260 pieces.add(node.value); | 253 pieces.add(node.value); |
| 261 super.visitSimpleStringLiteral(node); | 254 super.visitSimpleStringLiteral(node); |
| 262 } | 255 } |
| 263 | 256 |
| 264 void visitInterpolationString(InterpolationString node) { | 257 void visitInterpolationString(InterpolationString node) { |
| 265 pieces.add(node.value); | 258 pieces.add(node.value); |
| 266 super.visitInterpolationString(node); | 259 super.visitInterpolationString(node); |
| 267 } | 260 } |
| 268 | 261 |
| 269 // TODO(alanknight): The limitation to simple identifiers is important | |
| 270 // to avoid letting translators write arbitrary code, but is a problem | |
| 271 // for plurals. | |
| 272 void visitInterpolationExpression(InterpolationExpression node) { | 262 void visitInterpolationExpression(InterpolationExpression node) { |
| 273 if (node.expression is! SimpleIdentifier) { | 263 if (node.expression is SimpleIdentifier) { |
| 264 return handleSimpleInterpolation(node); |
| 265 } else { |
| 266 return lookForPluralOrGender(node); |
| 267 } |
| 268 // Note that we never end up calling super. |
| 269 } |
| 270 |
| 271 lookForPluralOrGender(InterpolationExpression node) { |
| 272 var visitor = new PluralAndGenderVisitor(pieces, message); |
| 273 node.accept(visitor); |
| 274 if (!visitor.foundPluralOrGender) { |
| 274 throw new IntlMessageExtractionException( | 275 throw new IntlMessageExtractionException( |
| 275 "Only simple identifiers are allowed in message " | 276 "Only simple identifiers and Intl.plural/gender/select expressions " |
| 276 "interpolation expressions.\nError at $node"); | 277 "are allowed in message " |
| 278 "interpolation expressions.\nError at $node"); |
| 277 } | 279 } |
| 280 } |
| 281 |
| 282 void handleSimpleInterpolation(InterpolationExpression node) { |
| 278 var index = arguments.indexOf(node.expression.toString()); | 283 var index = arguments.indexOf(node.expression.toString()); |
| 279 if (index == -1) { | 284 if (index == -1) { |
| 280 throw new IntlMessageExtractionException( | 285 throw new IntlMessageExtractionException( |
| 281 "Cannot find argument ${node.expression}"); | 286 "Cannot find argument ${node.expression}"); |
| 282 } | 287 } |
| 283 pieces.add(index); | 288 pieces.add(index); |
| 284 super.visitInterpolationExpression(node); | |
| 285 } | 289 } |
| 286 | 290 |
| 287 List get arguments => message.arguments; | 291 List get arguments => message.arguments; |
| 288 } | 292 } |
| 289 | 293 |
| 290 /** | 294 /** |
| 295 * A visitor to extract information from Intl.plural/gender sends. Note that |
| 296 * this is a SimpleASTVisitor, so it doesn't automatically recurse. So this |
| 297 * needs to be called where we expect a plural or gender immediately below. |
| 298 */ |
| 299 class PluralAndGenderVisitor extends SimpleASTVisitor { |
| 300 /** |
| 301 * A plural or gender always exists in the context of a parent message, |
| 302 * which could in turn also be a plural or gender. |
| 303 */ |
| 304 ComplexMessage parent; |
| 305 |
| 306 /** |
| 307 * The pieces of the message. We are given an initial version of this |
| 308 * from our parent and we add to it as we find additional information. |
| 309 */ |
| 310 List pieces; |
| 311 |
| 312 PluralAndGenderVisitor(this.pieces, this.parent) : super() {} |
| 313 |
| 314 /** This will be set to true if we find a plural or gender. */ |
| 315 bool foundPluralOrGender = false; |
| 316 |
| 317 visitInterpolationExpression(InterpolationExpression node) { |
| 318 // TODO(alanknight): Provide better errors for malformed expressions. |
| 319 if (!looksLikePluralOrGender(node.expression)) return; |
| 320 var reason = checkValidity(node.expression); |
| 321 if (reason != null) throw reason; |
| 322 var message = messageFromMethodInvocation(node.expression); |
| 323 foundPluralOrGender = true; |
| 324 pieces.add(message); |
| 325 super.visitInterpolationExpression(node); |
| 326 } |
| 327 |
| 328 /** Return true if [node] matches the pattern we expect for Intl.message() */ |
| 329 bool looksLikePluralOrGender(MethodInvocation node) { |
| 330 if (!["plural", "gender"].contains(node.methodName.name)) return false; |
| 331 if (!(node.target is SimpleIdentifier)) return false; |
| 332 SimpleIdentifier target = node.target; |
| 333 if (target.token.toString() != "Intl") return false; |
| 334 return true; |
| 335 } |
| 336 |
| 337 /** |
| 338 * Returns a String describing why the node is invalid, or null if no |
| 339 * reason is found, so it's presumed valid. |
| 340 */ |
| 341 String checkValidity(MethodInvocation node) { |
| 342 // TODO(alanknight): Add reasonable validity checks. |
| 343 } |
| 344 |
| 345 /** |
| 346 * Create a MainMessage from [node] using the name and |
| 347 * parameters of the last function/method declaration we encountered |
| 348 * and the parameters to the Intl.message call. |
| 349 */ |
| 350 messageFromMethodInvocation(MethodInvocation node) { |
| 351 var message; |
| 352 if (node.methodName.name == "gender") { |
| 353 message = new Gender(); |
| 354 } else if (node.methodName.name == "plural") { |
| 355 message = new Plural(); |
| 356 } else { |
| 357 throw new IntlMessageExtractionException("Invalid plural/gender message"); |
| 358 } |
| 359 message.parent = parent; |
| 360 |
| 361 var arguments = node.argumentList.arguments.elements; |
| 362 for (var arg in arguments.where((each) => each is NamedExpression)) { |
| 363 try { |
| 364 var interpolation = new InterpolationVisitor(message); |
| 365 arg.expression.accept(interpolation); |
| 366 message[arg.name.label.token.toString()] = interpolation.pieces; |
| 367 } on IntlMessageExtractionException catch (e) { |
| 368 message = null; |
| 369 print("Error $e"); |
| 370 print("Processing <$node>"); |
| 371 _reportErrorLocation(node); |
| 372 } |
| 373 } |
| 374 var mainArg = node.argumentList.arguments.elements.firstWhere( |
| 375 (each) => each is! NamedExpression); |
| 376 if (mainArg is SimpleStringLiteral) { |
| 377 message.mainArgument = mainArg.toString(); |
| 378 } else { |
| 379 message.mainArgument = mainArg.name; |
| 380 } |
| 381 return message; |
| 382 } |
| 383 } |
| 384 |
| 385 /** |
| 291 * Exception thrown when we cannot process a message properly. | 386 * Exception thrown when we cannot process a message properly. |
| 292 */ | 387 */ |
| 293 class IntlMessageExtractionException implements Exception { | 388 class IntlMessageExtractionException implements Exception { |
| 294 /** | 389 /** |
| 295 * A message describing the error. | 390 * A message describing the error. |
| 296 */ | 391 */ |
| 297 final String message; | 392 final String message; |
| 298 | 393 |
| 299 /** | 394 /** |
| 300 * Creates a new exception with an optional error [message]. | 395 * Creates a new exception with an optional error [message]. |
| 301 */ | 396 */ |
| 302 const IntlMessageExtractionException([this.message = ""]); | 397 const IntlMessageExtractionException([this.message = ""]); |
| 303 | 398 |
| 304 String toString() => "IntlMessageExtractionException: $message"; | 399 String toString() => "IntlMessageExtractionException: $message"; |
| 305 } | 400 } |
| OLD | NEW |