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 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 85 /** | 85 /** |
| 86 * We keep track of the data from the last MethodDeclaration, | 86 * We keep track of the data from the last MethodDeclaration, |
| 87 * FunctionDeclaration or FunctionExpression that we saw on the way down, | 87 * FunctionDeclaration or FunctionExpression that we saw on the way down, |
| 88 * 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. |
| 89 */ | 89 */ |
| 90 FormalParameterList parameters; | 90 FormalParameterList parameters; |
| 91 String name; | 91 String name; |
| 92 | 92 |
| 93 /** 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() */ |
| 94 bool looksLikeIntlMessage(MethodInvocation node) { | 94 bool looksLikeIntlMessage(MethodInvocation node) { |
| 95 if (node.methodName.name != "message") return false; | 95 const validNames = const ["message", "plural", "gender"]; |
| 96 if (!validNames.contains(node.methodName.name)) return false; | |
| 96 if (!(node.target is SimpleIdentifier)) return false; | 97 if (!(node.target is SimpleIdentifier)) return false; |
| 97 SimpleIdentifier target = node.target; | 98 SimpleIdentifier target = node.target; |
| 98 if (target.token.toString() != "Intl") return false; | 99 if (target.token.toString() != "Intl") return false; |
| 99 return true; | 100 return true; |
| 100 } | 101 } |
| 101 | 102 |
| 102 /** | 103 /** |
| 103 * Returns a String describing why the node is invalid, or null if no | 104 * Returns a String describing why the node is invalid, or null if no |
| 104 * reason is found, so it's presumed valid. | 105 * reason is found, so it's presumed valid. |
| 105 */ | 106 */ |
| 106 String checkValidity(MethodInvocation node) { | 107 String checkValidity(MethodInvocation node) { |
| 107 // The containing function cannot have named parameters. | 108 // The containing function cannot have named parameters. |
| 108 if (parameters.parameters.any((each) => each.kind == ParameterKind.NAMED)) { | 109 if (parameters.parameters.any((each) => each.kind == ParameterKind.NAMED)) { |
| 109 return "Named parameters on message functions are not supported."; | 110 return "Named parameters on message functions are not supported."; |
| 110 } | 111 } |
| 111 var arguments = node.argumentList.arguments; | 112 var arguments = node.argumentList.arguments; |
| 112 if (!(arguments.first is StringLiteral)) { | 113 |
| 113 return "Intl.message messages must be string literals"; | 114 if (node.methodName.name == 'message') { |
| 115 if (!(arguments.first is StringLiteral)) { | |
| 116 return "Intl.message messages must be string literals"; | |
| 117 } | |
| 114 } | 118 } |
| 119 | |
| 115 var namedArguments = arguments.skip(1); | 120 var namedArguments = arguments.skip(1); |
| 116 // This seems unlikely to happen, but make sure all are NamedExpression | 121 // This seems unlikely to happen, but make sure all are NamedExpression |
| 117 // before doing the tests below. | 122 // before doing the tests below. |
| 118 if (!namedArguments.every((each) => each is NamedExpression)) { | 123 if (!namedArguments.every((each) => each is NamedExpression)) { |
| 119 return "Message arguments except the message must be named"; | 124 return "Message arguments except the message must be named"; |
| 120 } | 125 } |
| 121 var notArgs = namedArguments.where( | 126 var notArgs = namedArguments.where( |
| 122 (each) => each.name.label.name != 'args'); | 127 (each) => each.name.label.name != 'args'); |
| 123 var values = notArgs.map((each) => each.expression).toList(); | 128 var values = notArgs.map((each) => each.expression).toList(); |
| 124 if (!values.every((each) => each is SimpleStringLiteral)) { | 129 if (!values.every((each) => each is SimpleStringLiteral)) { |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 160 * encountered before seeing the Intl.message call. | 165 * encountered before seeing the Intl.message call. |
| 161 */ | 166 */ |
| 162 void visitFunctionDeclaration(FunctionDeclaration node) { | 167 void visitFunctionDeclaration(FunctionDeclaration node) { |
| 163 parameters = node.functionExpression.parameters; | 168 parameters = node.functionExpression.parameters; |
| 164 name = node.name.name; | 169 name = node.name.name; |
| 165 super.visitFunctionDeclaration(node); | 170 super.visitFunctionDeclaration(node); |
| 166 } | 171 } |
| 167 | 172 |
| 168 /** | 173 /** |
| 169 * Examine method invocations to see if they look like calls to Intl.message. | 174 * Examine method invocations to see if they look like calls to Intl.message. |
| 175 * If we've found one, stop recursing. This is important because we can have | |
| 176 * Intl.message(...Intl.plural...) and we don't want to treat the inner | |
| 177 * plural as if it was an outermost message. | |
| 170 */ | 178 */ |
| 171 void visitMethodInvocation(MethodInvocation node) { | 179 void visitMethodInvocation(MethodInvocation node) { |
| 172 addIntlMessage(node); | 180 if (!addIntlMessage(node)) { |
| 173 return super.visitNode(node); | 181 return super.visitMethodInvocation(node); |
|
Alan Knight
2013/07/23 23:24:29
I changed the super call here because it just seem
| |
| 182 } | |
| 174 } | 183 } |
| 175 | 184 |
| 176 /** | 185 /** |
| 177 * Check that the node looks like an Intl.message invocation, and create | 186 * Check that the node looks like an Intl.message invocation, and create |
| 178 * the [IntlMessage] object from it and store it in [messages]. | 187 * the [IntlMessage] object from it and store it in [messages]. |
|
Emily Fortuna
2013/07/24 18:35:51
can you update the comment here and explain what
Alan Knight
2013/07/24 19:50:10
Done. Which also pointed out that I was returning
| |
| 179 */ | 188 */ |
| 180 void addIntlMessage(MethodInvocation node) { | 189 bool addIntlMessage(MethodInvocation node) { |
| 181 if (!looksLikeIntlMessage(node)) return; | 190 if (!looksLikeIntlMessage(node)) return false; |
| 182 var reason = checkValidity(node); | 191 var reason = checkValidity(node); |
| 183 if (reason != null && !suppressWarnings) { | 192 if (reason != null) { |
|
Alan Knight
2013/07/23 23:24:29
If suppress warnings was on, this was including th
| |
| 184 print("Skipping invalid Intl.message invocation\n <$node>"); | 193 if (!suppressWarnings) { |
| 185 print(" reason: $reason"); | 194 print("Skipping invalid Intl.message invocation\n <$node>"); |
| 186 _reportErrorLocation(node); | 195 print(" reason: $reason"); |
| 187 return; | 196 _reportErrorLocation(node); |
| 197 } | |
| 198 return false; | |
| 188 } | 199 } |
| 189 var message = messageFromMethodInvocation(node); | 200 var message; |
| 201 if (node.methodName.name == "message") { | |
| 202 message = messageFromIntlMessageCall(node); | |
| 203 } else { | |
| 204 message = messageFromDirectPluralOrGenderCall(node); | |
| 205 } | |
| 190 if (message != null) messages[message.name] = message; | 206 if (message != null) messages[message.name] = message; |
| 207 return true; | |
| 191 } | 208 } |
| 192 | 209 |
| 193 /** | 210 /** |
| 194 * Create an IntlMessage from [node] using the name and | 211 * Create an IntlMessage from [node] using the name and |
| 195 * parameters of the last function/method declaration we encountered | 212 * parameters of the last function/method declaration we encountered |
| 196 * and the parameters to the Intl.message call. | 213 * and the parameters to the Intl.message call. |
| 197 */ | 214 */ |
| 198 MainMessage messageFromMethodInvocation(MethodInvocation node) { | 215 MainMessage messageFromIntlMessageCall(MethodInvocation node) { |
| 199 var message = new MainMessage(); | 216 var message = new MainMessage(); |
| 200 message.name = name; | 217 message.name = name; |
| 201 message.arguments = parameters.parameters.elements.map( | 218 message.arguments = parameters.parameters.elements.map( |
| 202 (x) => x.identifier.name).toList(); | 219 (x) => x.identifier.name).toList(); |
| 203 var arguments = node.argumentList.arguments.elements; | 220 var arguments = node.argumentList.arguments.elements; |
| 204 try { | 221 try { |
| 205 var interpolation = new InterpolationVisitor(message); | 222 var interpolation = new InterpolationVisitor(message); |
| 206 arguments.first.accept(interpolation); | 223 arguments.first.accept(interpolation); |
| 207 message.messagePieces.addAll(interpolation.pieces); | 224 message.messagePieces.addAll(interpolation.pieces); |
| 208 } on IntlMessageExtractionException catch (e) { | 225 } on IntlMessageExtractionException catch (e) { |
| 209 message = null; | 226 message = null; |
| 210 print("Error $e"); | 227 print("Error $e"); |
| 211 print("Processing <$node>"); | 228 print("Processing <$node>"); |
| 212 _reportErrorLocation(node); | 229 _reportErrorLocation(node); |
| 213 } | 230 } |
| 214 for (NamedExpression namedArgument in arguments.skip(1)) { | 231 for (NamedExpression namedArgument in arguments.skip(1)) { |
| 215 var name = namedArgument.name.label.name; | 232 var name = namedArgument.name.label.name; |
| 216 var exp = namedArgument.expression; | 233 var exp = namedArgument.expression; |
| 217 var string = exp is SimpleStringLiteral ? exp.value : exp.toString(); | 234 var string = exp is SimpleStringLiteral ? exp.value : exp.toString(); |
| 218 message[name] = string; | 235 message[name] = string; |
| 219 } | 236 } |
| 220 return message; | 237 return message; |
| 221 } | 238 } |
| 239 | |
| 240 /** | |
| 241 * Create an IntlMessage from [node] using the name and | |
| 242 * parameters of the last function/method declaration we encountered | |
| 243 * and the parameters to the Intl.message call. | |
| 244 */ | |
| 245 MainMessage messageFromDirectPluralOrGenderCall(MethodInvocation node) { | |
| 246 // TODO(alanknight): Refactor this to reduce code duplication. | |
|
Emily Fortuna
2013/07/24 18:35:51
why not refactor now? :-)
Alan Knight
2013/07/24 19:50:10
Because I looked a bit and it didn't look easy. Bu
| |
| 247 var message = new MainMessage(); | |
| 248 message.name = name; | |
| 249 message.arguments = parameters.parameters.elements.map( | |
| 250 (x) => x.identifier.name).toList(); | |
| 251 var arguments = node.argumentList.arguments.elements; | |
| 252 var visitor = new PluralAndGenderVisitor(message.messagePieces, message); | |
| 253 node.accept(visitor); | |
| 254 var pluralOrGender = message.messagePieces.last; | |
| 255 for (NamedExpression namedArgument in arguments.skip(1)) { | |
| 256 var name = namedArgument.name.label.name; | |
| 257 var exp = namedArgument.expression; | |
| 258 var string = exp is SimpleStringLiteral ? exp.value : exp.toString(); | |
| 259 if (["name", "desc", "examples", "args"].contains(name)) { | |
| 260 message[name] = string; | |
| 261 } else { | |
| 262 pluralOrGender[name] = string; | |
| 263 } | |
| 264 } | |
| 265 return message; | |
| 266 } | |
| 222 } | 267 } |
| 223 | 268 |
| 224 /** | 269 /** |
| 225 * Given an interpolation, find all of its chunks, validate that they are only | 270 * Given an interpolation, find all of its chunks, validate that they are only |
| 226 * simple variable substitutions or else Intl.plural/gender calls, | 271 * simple variable substitutions or else Intl.plural/gender calls, |
| 227 * and keep track of the pieces of text so that other parts | 272 * and keep track of the pieces of text so that other parts |
| 228 * of the program can deal with the simple string sections and the generated | 273 * 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 | 274 * parts separately. Note that this is a SimpleASTVisitor, so it only |
| 230 * traverses one level of children rather than automatically recursing. If we | 275 * 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 | 276 * find a plural or gender, which requires recursion, we do it with a separate |
| (...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 318 // TODO(alanknight): Provide better errors for malformed expressions. | 363 // TODO(alanknight): Provide better errors for malformed expressions. |
| 319 if (!looksLikePluralOrGender(node.expression)) return; | 364 if (!looksLikePluralOrGender(node.expression)) return; |
| 320 var reason = checkValidity(node.expression); | 365 var reason = checkValidity(node.expression); |
| 321 if (reason != null) throw reason; | 366 if (reason != null) throw reason; |
| 322 var message = messageFromMethodInvocation(node.expression); | 367 var message = messageFromMethodInvocation(node.expression); |
| 323 foundPluralOrGender = true; | 368 foundPluralOrGender = true; |
| 324 pieces.add(message); | 369 pieces.add(message); |
| 325 super.visitInterpolationExpression(node); | 370 super.visitInterpolationExpression(node); |
| 326 } | 371 } |
| 327 | 372 |
| 328 /** Return true if [node] matches the pattern we expect for Intl.message() */ | 373 visitMethodInvocation(MethodInvocation node) { |
| 374 pieces.add(messageFromMethodInvocation(node)); | |
| 375 super.visitMethodInvocation(node); | |
| 376 } | |
| 377 | |
| 378 /** Return true if [node] matches the pattern for plural or gender message.*/ | |
| 329 bool looksLikePluralOrGender(MethodInvocation node) { | 379 bool looksLikePluralOrGender(MethodInvocation node) { |
| 330 if (!["plural", "gender"].contains(node.methodName.name)) return false; | 380 if (!["plural", "gender"].contains(node.methodName.name)) return false; |
| 331 if (!(node.target is SimpleIdentifier)) return false; | 381 if (!(node.target is SimpleIdentifier)) return false; |
| 332 SimpleIdentifier target = node.target; | 382 SimpleIdentifier target = node.target; |
| 333 if (target.token.toString() != "Intl") return false; | 383 if (target.token.toString() != "Intl") return false; |
| 334 return true; | 384 return true; |
| 335 } | 385 } |
| 336 | 386 |
| 337 /** | 387 /** |
| 338 * Returns a String describing why the node is invalid, or null if no | 388 * Returns a String describing why the node is invalid, or null if no |
| 339 * reason is found, so it's presumed valid. | 389 * reason is found, so it's presumed valid. |
| 340 */ | 390 */ |
| 341 String checkValidity(MethodInvocation node) { | 391 String checkValidity(MethodInvocation node) { |
| 342 // TODO(alanknight): Add reasonable validity checks. | 392 // TODO(alanknight): Add reasonable validity checks. |
| 343 } | 393 } |
| 344 | 394 |
| 345 /** | 395 /** |
| 346 * Create a MainMessage from [node] using the name and | 396 * Create a MainMessage from [node] using the name and |
| 347 * parameters of the last function/method declaration we encountered | 397 * parameters of the last function/method declaration we encountered |
| 348 * and the parameters to the Intl.message call. | 398 * and the parameters to the Intl.message call. |
| 349 */ | 399 */ |
| 350 messageFromMethodInvocation(MethodInvocation node) { | 400 Message messageFromMethodInvocation(MethodInvocation node) { |
| 351 var message; | 401 var message; |
| 352 if (node.methodName.name == "gender") { | 402 if (node.methodName.name == "gender") { |
| 353 message = new Gender(); | 403 message = new Gender(); |
| 354 } else if (node.methodName.name == "plural") { | 404 } else if (node.methodName.name == "plural") { |
| 355 message = new Plural(); | 405 message = new Plural(); |
| 356 } else { | 406 } else { |
| 357 throw new IntlMessageExtractionException("Invalid plural/gender message"); | 407 throw new IntlMessageExtractionException("Invalid plural/gender message"); |
| 358 } | 408 } |
| 359 message.parent = parent; | 409 message.parent = parent; |
| 360 | 410 |
| (...skipping 30 matching lines...) Expand all Loading... | |
| 391 */ | 441 */ |
| 392 final String message; | 442 final String message; |
| 393 | 443 |
| 394 /** | 444 /** |
| 395 * Creates a new exception with an optional error [message]. | 445 * Creates a new exception with an optional error [message]. |
| 396 */ | 446 */ |
| 397 const IntlMessageExtractionException([this.message = ""]); | 447 const IntlMessageExtractionException([this.message = ""]); |
| 398 | 448 |
| 399 String toString() => "IntlMessageExtractionException: $message"; | 449 String toString() => "IntlMessageExtractionException: $message"; |
| 400 } | 450 } |
| OLD | NEW |