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