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 |