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 |