Index: pkg/intl/lib/extract_messages.dart |
diff --git a/pkg/intl/lib/extract_messages.dart b/pkg/intl/lib/extract_messages.dart |
old mode 100755 |
new mode 100644 |
index 1ca9e13d02ba72a04d1d99eec5623d3f5d28bb28..64378a0a2a127bf268e0d5358ea0b31a96e98d6e |
--- a/pkg/intl/lib/extract_messages.dart |
+++ b/pkg/intl/lib/extract_messages.dart |
@@ -36,39 +36,52 @@ bool suppressWarnings = false; |
* Parse the source of the Dart program file [file] and return a Map from |
* message names to [IntlMessage] instances. |
*/ |
-Map<String, IntlMessage> parseFile(File file) { |
- var unit = parseDartFile(file.path); |
- var visitor = new MessageFindingVisitor(unit, file.path); |
- unit.accept(visitor); |
+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
|
+ _root = parseDartFile(file.path); |
+ _origin = file.path; |
+ var visitor = new MessageFindingVisitor(); |
+ _root.accept(visitor); |
return visitor.messages; |
} |
+ |
Emily Fortuna
2013/07/03 17:52:33
remove line here
Alan Knight
2013/07/03 18:41:07
Done.
|
/** |
- * This visits the program source nodes looking for Intl.message uses |
- * that conform to its pattern and then finding the |
+ * The root of the compilation unit, and the first node we visit. We hold |
+ * on to this for error reporting, as it can give us line numbers of other |
+ * nodes. |
*/ |
-class MessageFindingVisitor extends GeneralizingASTVisitor { |
+CompilationUnit _root; |
- /** |
- * The root of the compilation unit, and the first node we visit. We hold |
- * on to this for error reporting, as it can give us line numbers of other |
- * nodes. |
- */ |
- final CompilationUnit root; |
+/** |
+ * An arbitrary string describing where the source code came from. Most |
+ * obviously, this could be a file path. We use this when reporting |
+ * invalid messages. |
+ */ |
+String _origin; |
+ |
+void _reportErrorLocation(ASTNode node) { |
+ if (_origin != null) print(" from $_origin"); |
+ var info = _root.lineInfo; |
+ if (info != null) { |
+ var line = info.getLocation(node.offset); |
+ print(" line: ${line.lineNumber}, column: ${line.columnNumber}"); |
+ } |
+} |
- /** |
- * An arbitrary string describing where the source code came from. Most |
- * obviously, this could be a file path. We use this when reporting |
- * invalid messages. |
- */ |
- final String origin; |
+/** |
+ * This visits the program source nodes looking for Intl.message uses |
+ * that conform to its pattern and then creating the corresponding |
+ * IntlMessage objects. We have to find both the enclosing function, and |
+ * the Intl.message invocation. |
+ */ |
+class MessageFindingVisitor extends GeneralizingASTVisitor { |
- MessageFindingVisitor(this.root, this.origin); |
+ MessageFindingVisitor(); |
/** |
- * Accumulates the messages we have found. |
+ * Accumulates the messages we have found, keyed by name. |
*/ |
- final Map<String, IntlMessage> messages = new Map<String, IntlMessage>(); |
+ final Map<String, MainMessage> messages = new Map<String, MainMessage>(); |
/** |
* We keep track of the data from the last MethodDeclaration, |
@@ -171,7 +184,7 @@ class MessageFindingVisitor extends GeneralizingASTVisitor { |
if (reason != null && !suppressWarnings) { |
print("Skipping invalid Intl.message invocation\n <$node>"); |
print(" reason: $reason"); |
- reportErrorLocation(node); |
+ _reportErrorLocation(node); |
return; |
} |
var message = messageFromMethodInvocation(node); |
@@ -183,79 +196,60 @@ class MessageFindingVisitor extends GeneralizingASTVisitor { |
* parameters of the last function/method declaration we encountered |
* and the parameters to the Intl.message call. |
*/ |
- IntlMessage messageFromMethodInvocation(MethodInvocation node) { |
- var message = new IntlMessage(); |
+ MainMessage messageFromMethodInvocation(MethodInvocation node) { |
+ var message = new MainMessage(); |
message.name = name; |
message.arguments = parameters.parameters.elements.map( |
(x) => x.identifier.name).toList(); |
+ var arguments = node.argumentList.arguments.elements; |
try { |
- node.accept(new MessageVisitor(message)); |
+ var interpolation = new InterpolationVisitor(message); |
+ arguments.first.accept(interpolation); |
+ message.messagePieces.addAll(interpolation.pieces); |
} on IntlMessageExtractionException catch (e) { |
message = null; |
print("Error $e"); |
print("Processing <$node>"); |
- reportErrorLocation(node); |
+ _reportErrorLocation(node); |
} |
- return message; |
- } |
- |
- void reportErrorLocation(ASTNode node) { |
- if (origin != null) print(" from $origin"); |
- var info = root.lineInfo; |
- if (info != null) { |
- var line = info.getLocation(node.offset); |
- print(" line: ${line.lineNumber}, column: ${line.columnNumber}"); |
+ for (NamedExpression namedArgument in arguments.skip(1)) { |
+ var name = namedArgument.name.label.name; |
+ var exp = namedArgument.expression; |
+ var string = exp is SimpleStringLiteral ? exp.value : exp.toString(); |
+ message[name] = string; |
} |
- } |
-} |
- |
-/** |
- * Given a node that looks like an invocation of Intl.message, extract out |
- * the message and the parameters and store them in [target]. |
- */ |
-class MessageVisitor extends GeneralizingASTVisitor { |
- IntlMessage target; |
- |
- MessageVisitor(IntlMessage this.target); |
- |
- /** |
- * Extract out the message string. If it's an interpolation, turn it into |
- * a single string with interpolation characters. |
- */ |
- void visitArgumentList(ArgumentList node) { |
- var interpolation = new InterpolationVisitor(target); |
- node.arguments.elements.first.accept(interpolation); |
- target.messagePieces = interpolation.pieces; |
- super.visitArgumentList(node); |
- } |
- |
- /** |
- * Find the values of all the named arguments, remove quotes, and save them |
- * into [target]. |
- */ |
- void visitNamedExpression(NamedExpression node) { |
- var name = node.name.label.name; |
- var exp = node.expression; |
- var string = exp is SimpleStringLiteral ? exp.value : exp.toString(); |
- target[name] = string; |
- super.visitNamedExpression(node); |
+ return message; |
} |
} |
/** |
* Given an interpolation, find all of its chunks, validate that they are only |
- * simple interpolations, and keep track of the chunks so that other parts |
- * of the program can deal with the interpolations and the simple string |
- * sections separately. |
+ * simple variable substitutions or else Intl.plural/gender calls, |
+ * and keep track of the pieces of text so that other parts |
+ * of the program can deal with the simple string sections and the generated |
+ * parts separately. Note that this is a SimpleASTVisitor, so it only |
+ * traverses one level of children rather than automatically recursing. If we |
+ * find a plural or gender, which requires recursion, we do it with a separate |
+ * special-purpose visitor. |
*/ |
-class InterpolationVisitor extends GeneralizingASTVisitor { |
- IntlMessage message; |
+class InterpolationVisitor extends SimpleASTVisitor { |
+ Message message; |
InterpolationVisitor(this.message); |
List pieces = []; |
String get extractedMessage => pieces.join(); |
+ void visitAdjacentStrings(AdjacentStrings node) { |
+ node.visitChildren(this); |
+ super.visitAdjacentStrings(node); |
+ } |
+ |
+ void visitStringInterpolation(StringInterpolation node) { |
+ node.visitChildren(this); |
+ super.visitStringInterpolation(node); |
+ } |
+ |
void visitSimpleStringLiteral(SimpleStringLiteral node) { |
pieces.add(node.value); |
super.visitSimpleStringLiteral(node); |
@@ -266,28 +260,130 @@ class InterpolationVisitor extends GeneralizingASTVisitor { |
super.visitInterpolationString(node); |
} |
- // TODO(alanknight): The limitation to simple identifiers is important |
- // to avoid letting translators write arbitrary code, but is a problem |
- // for plurals. |
void visitInterpolationExpression(InterpolationExpression node) { |
- if (node.expression is! SimpleIdentifier) { |
+ if (node.expression is SimpleIdentifier) { |
+ return handleSimpleInterpolation(node); |
+ } else { |
+ return lookForPluralOrGender(node); |
+ } |
+ // Note that we never end up calling super. |
+ } |
+ |
+ lookForPluralOrGender(InterpolationExpression node) { |
+ var visitor = new PluralAndGenderVisitor(pieces, message); |
+ node.accept(visitor); |
+ if (!visitor.foundSomething) { |
throw new IntlMessageExtractionException( |
- "Only simple identifiers are allowed in message " |
- "interpolation expressions.\nError at $node"); |
+ "Only simple identifiers and Intl.plural/gender/select expressions " |
+ "are allowed in message " |
+ "interpolation expressions.\nError at $node"); |
} |
+ } |
+ |
+ void handleSimpleInterpolation(InterpolationExpression node) { |
var index = arguments.indexOf(node.expression.toString()); |
if (index == -1) { |
throw new IntlMessageExtractionException( |
"Cannot find argument ${node.expression}"); |
} |
pieces.add(index); |
- super.visitInterpolationExpression(node); |
} |
List get arguments => message.arguments; |
} |
/** |
+ * A visitor to extract information from Intl.plural/gender sends. Note that |
+ * this is a SimpleASTVisitor, so it doesn't automatically recurse. So this |
+ * needs to be called where we expect a plural or gender immediately below. |
+ */ |
+class PluralAndGenderVisitor extends SimpleASTVisitor { |
+ /** |
+ * A plural or gender always exists in the context of a parent message, |
+ * which could in turn also be a plural or gender. |
+ */ |
+ ComplexMessage parent; |
+ |
+ /** |
+ * The pieces of the message. We are given an initial version of this |
+ * from our parent and we add to it as we find additional information. |
+ */ |
+ List pieces; |
+ |
+ PluralAndGenderVisitor(this.pieces, this.parent) : super() {} |
+ |
+ /** This will be set to true if we find a plural or gender. */ |
+ 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.
|
+ |
+ visitInterpolationExpression(InterpolationExpression node) { |
+ // TODO(alanknight): Provide better errors for malformed expressions. |
+ 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.
|
+ var reason = checkValidity(node.expression); |
+ if (reason != null) throw reason; |
+ var message = messageFromMethodInvocation(node.expression); |
+ foundSomething = true; |
+ pieces.add(message); |
+ super.visitInterpolationExpression(node); |
+ } |
+ |
+ /** Return true if [node] matches the pattern we expect for Intl.message() */ |
+ bool looksLikePluralOrGender(MethodInvocation node) { |
+ if (!["plural", "gender"].contains(node.methodName.name)) return false; |
+ if (!(node.target is SimpleIdentifier)) return false; |
+ SimpleIdentifier target = node.target; |
+ if (target.token.toString() != "Intl") return false; |
+ return true; |
+ } |
+ |
+ /** |
+ * Returns a String describing why the node is invalid, or null if no |
+ * reason is found, so it's presumed valid. |
+ */ |
+ String checkValidity(MethodInvocation node) { |
+ // 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.
|
+ } |
+ |
+ /** |
+ * Create a MainMessage from [node] using the name and |
+ * parameters of the last function/method declaration we encountered |
+ * and the parameters to the Intl.message call. |
+ */ |
+ messageFromMethodInvocation(MethodInvocation node) { |
+ var message; |
+ if (node.methodName.name == "gender") { |
+ message = new Gender(); |
+ } else if (node.methodName.name == "plural") { |
+ message = new Plural(); |
+ } else { |
+ throw new IntlMessageExtractionException("Invalid plural/gender message"); |
+ } |
+ message.parent = parent; |
+ |
+ var arguments = node.argumentList.arguments.elements; |
+ for (var arg in arguments.where((each) => each is NamedExpression)) { |
+ try { |
+ var interpolation = new InterpolationVisitor(message); |
+ arg.expression.accept(interpolation); |
+ message[arg.name.label.token.toString()] = interpolation.pieces; |
+ } on IntlMessageExtractionException catch (e) { |
+ message = null; |
+ print("Error $e"); |
+ print("Processing <$node>"); |
+ _reportErrorLocation(node); |
+ } |
+ } |
+ var mainArg = node.argumentList.arguments.elements.firstWhere( |
+ (each) => each is! NamedExpression); |
+ if (mainArg is SimpleStringLiteral) { |
+ message.mainArgument = mainArg.toString(); |
+ } else { |
+ message.mainArgument = mainArg.name; |
+ } |
+ return message; |
+ } |
+} |
+ |
+/** |
* Exception thrown when we cannot process a message properly. |
*/ |
class IntlMessageExtractionException implements Exception { |