| Index: pkg/intl/lib/extract_messages.dart
|
| diff --git a/pkg/intl/lib/extract_messages.dart b/pkg/intl/lib/extract_messages.dart
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..fb57341bcb159a6814804312aff2e9e8d97d6a49
|
| --- /dev/null
|
| +++ b/pkg/intl/lib/extract_messages.dart
|
| @@ -0,0 +1,332 @@
|
| +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
|
| +// for details. All rights reserved. Use of this source code is governed by a
|
| +// BSD-style license that can be found in the LICENSE file.
|
| +
|
| +/**
|
| + * This is for use in extracting messages from a Dart program
|
| + * using the Intl.message() mechanism and writing them to a file for
|
| + * translation. This provides only the stub of a mechanism, because it
|
| + * doesn't define how the file should be written. It provides an
|
| + * [IntlMessage] class that holds the extracted data and [parseString]
|
| + * and [parseFile] methods which
|
| + * can extract messages that conform to the expected pattern:
|
| + * (parameters) => Intl.message("Message $parameters", desc: ...);
|
| + * It uses the analyzer_experimental package to do the parsing, so may
|
| + * break if there are changes to the API that it provides.
|
| + * An example can be found in test/message_extraction/extract_to_json.dart
|
| + *
|
| + * Note that this does not understand how to follow part directives, so it
|
| + * has to explicitly be given all the files that it needs. A typical use case
|
| + * is to run it on all .dart files in a directory.
|
| + */
|
| +library extract_messages;
|
| +
|
| +import 'package:analyzer_experimental/src/generated/ast.dart';
|
| +import 'package:analyzer_experimental/src/generated/error.dart';
|
| +import 'package:analyzer_experimental/src/generated/java_core.dart';
|
| +import 'package:analyzer_experimental/src/generated/parser.dart';
|
| +import 'package:analyzer_experimental/src/generated/scanner.dart';
|
| +import 'package:analyzer_experimental/src/generated/source.dart';
|
| +import 'package:analyzer_experimental/src/generated/utilities_dart.dart';
|
| +import 'dart:io';
|
| +import 'package:intl/src/intl_message.dart';
|
| +
|
| +/**
|
| + * Parse the dart program represented in [sourceCode] and return a Map from
|
| + * message names to [IntlMessage] instances. The [origin] is a string
|
| + * describing where the source came from, and is used in error messages.
|
| + */
|
| +Map<String, IntlMessage> parseString(String sourceCode, [String origin]) {
|
| + var errorListener = new _ErrorCollector();
|
| + var scanner = new StringScanner(null, sourceCode, errorListener);
|
| + var token = scanner.tokenize();
|
| + var parser = new Parser(null, errorListener);
|
| + var unit = parser.parseCompilationUnit(token);
|
| + unit.lineInfo = new LineInfo(scanner.lineStarts);
|
| +
|
| + var visitor = new MessageFindingVisitor(unit, origin);
|
| + unit.accept(visitor);
|
| + for (var error in errorListener.errors) {
|
| + print(error);
|
| + }
|
| + return visitor.messages;
|
| +}
|
| +
|
| +/**
|
| + * 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 sourceCode = file.readAsStringSync();
|
| + return parseString(sourceCode, file.path);
|
| +}
|
| +
|
| +/**
|
| + * An error handler for parsing. Error handling is currently very primitive.
|
| + */
|
| +class _ErrorCollector extends AnalysisErrorListener {
|
| + List<AnalysisError> errors;
|
| + _ErrorCollector() : errors = new List<AnalysisError>();
|
| + onError(error) => errors.add(error);
|
| +}
|
| +
|
| +/**
|
| + * This visits the program source nodes looking for Intl.message uses
|
| + * that conform to its pattern and then finding the
|
| + */
|
| +class MessageFindingVisitor extends GeneralizingASTVisitor {
|
| +
|
| + /**
|
| + * 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.
|
| + */
|
| + final String origin;
|
| +
|
| + MessageFindingVisitor(this.root, this.origin);
|
| +
|
| + /**
|
| + * Accumulates the messages we have found.
|
| + */
|
| + final Map<String, IntlMessage> messages = new Map<String, IntlMessage>();
|
| +
|
| + /**
|
| + * We keep track of the data from the last MethodDeclaration,
|
| + * FunctionDeclaration or FunctionExpression that we saw on the way down,
|
| + * as that will be the nearest parent of the Intl.message invocation.
|
| + */
|
| + FormalParameterList parameters;
|
| + String name;
|
| +
|
| + /** Return true if [node] matches the pattern we expect for Intl.message() */
|
| + bool looksLikeIntlMessage(MethodInvocation node) {
|
| + if (node.methodName.name != "message") 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) {
|
| + // The containing function cannot have named parameters.
|
| + if (parameters.parameters.any((each) => each.kind == ParameterKind.NAMED)) {
|
| + return "Named parameters on message functions are not supported.";
|
| + }
|
| + var arguments = node.argumentList.arguments;
|
| + if (!(arguments.first is StringLiteral)) {
|
| + return "Intl.message messages must be string literals";
|
| + }
|
| + var namedArguments = arguments.skip(1);
|
| + // This seems unlikely to happen, but make sure all are NamedExpression
|
| + // before doing the tests below.
|
| + if (!namedArguments.every((each) => each is NamedExpression)) {
|
| + return "Message arguments except the message must be named";
|
| + }
|
| + var notArgs = namedArguments.where(
|
| + (each) => each.name.label.name != 'args');
|
| + var values = notArgs.map((each) => each.expression).toList();
|
| + if (!values.every((each) => each is SimpleStringLiteral)) {
|
| + "Intl.message arguments must be simple string literals";
|
| + }
|
| + if (!notArgs.any((each) => each.name.label.name == 'name')) {
|
| + return "The 'name' argument for Intl.message must be specified";
|
| + }
|
| + var hasArgs = namedArguments.any((each) => each.name.label.name == 'args');
|
| + var hasParameters = !parameters.parameters.isEmpty;
|
| + if (!hasArgs && hasParameters) {
|
| + return "The 'args' argument for Intl.message must be specified";
|
| + }
|
| + return null;
|
| + }
|
| +
|
| + /**
|
| + * Record the parameters of the function or method declaration we last
|
| + * encountered before seeing the Intl.message call.
|
| + */
|
| + void visitMethodDeclaration(MethodDeclaration node) {
|
| + parameters = node.parameters;
|
| + String name = node.name.name;
|
| + super.visitMethodDeclaration(node);
|
| + }
|
| +
|
| + /**
|
| + * Record the parameters of the function or method declaration we last
|
| + * encountered before seeing the Intl.message call.
|
| + */
|
| + void visitFunctionExpression(FunctionExpression node) {
|
| + parameters = node.parameters;
|
| + name = null;
|
| + super.visitFunctionExpression(node);
|
| + }
|
| +
|
| + /**
|
| + * Record the parameters of the function or method declaration we last
|
| + * encountered before seeing the Intl.message call.
|
| + */
|
| + void visitFunctionDeclaration(FunctionDeclaration node) {
|
| + parameters = node.functionExpression.parameters;
|
| + name = node.name.name;
|
| + super.visitFunctionDeclaration(node);
|
| + }
|
| +
|
| + /**
|
| + * Examine method invocations to see if they look like calls to Intl.message.
|
| + */
|
| + void visitMethodInvocation(MethodInvocation node) {
|
| + addIntlMessage(node);
|
| + return super.visitNode(node);
|
| + }
|
| +
|
| + /**
|
| + * Check that the node looks like an Intl.message invocation, and create
|
| + * the [IntlMessage] object from it and store it in [messages].
|
| + */
|
| + void addIntlMessage(MethodInvocation node) {
|
| + if (!looksLikeIntlMessage(node)) return;
|
| + var reason = checkValidity(node);
|
| + if (!(reason == null)) {
|
| + print("Skipping invalid Intl.message invocation\n <$node>");
|
| + print(" reason: $reason");
|
| + reportErrorLocation(node);
|
| + return;
|
| + }
|
| + var message = messageFromMethodInvocation(node);
|
| + if (message != null) messages[message.name] = message;
|
| + }
|
| +
|
| + /**
|
| + * Create an IntlMessage from [node] using the name and
|
| + * 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();
|
| + message.name = name;
|
| + message.arguments = parameters.parameters.elements.map(
|
| + (x) => x.identifier.name).toList();
|
| + try {
|
| + node.accept(new MessageVisitor(message));
|
| + } on IntlMessageExtractionException catch (e) {
|
| + message = null;
|
| + print("Error $e");
|
| + print("Processing <$node>");
|
| + reportErrorLocation(node);
|
| + }
|
| + return message;
|
| + }
|
| +
|
| + void reportErrorLocation(ASTNode node) {
|
| + if (origin != null) print("from $origin");
|
| + LineInfo info = root.lineInfo;
|
| + if (info != null) {
|
| + LineInfo_Location line = info.getLocation(node.offset);
|
| + print("line: ${line.lineNumber}, column: ${line.columnNumber}");
|
| + }
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * 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);
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * 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.
|
| + */
|
| +class InterpolationVisitor extends GeneralizingASTVisitor {
|
| + IntlMessage message;
|
| +
|
| + InterpolationVisitor(this.message);
|
| +
|
| + List pieces = [];
|
| + String get extractedMessage => pieces.join();
|
| +
|
| + void visitSimpleStringLiteral(SimpleStringLiteral node) {
|
| + pieces.add(node.value);
|
| + super.visitSimpleStringLiteral(node);
|
| + }
|
| +
|
| + void visitInterpolationString(InterpolationString node) {
|
| + pieces.add(node.value);
|
| + 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) {
|
| + throw new IntlMessageExtractionException(
|
| + "Only simple identifiers are allowed in message "
|
| + "interpolation expressions.\nError at $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;
|
| +}
|
| +
|
| +/**
|
| + * Exception thrown when we cannot process a message properly.
|
| + */
|
| +class IntlMessageExtractionException implements Exception {
|
| + /**
|
| + * A message describing the error.
|
| + */
|
| + final String message;
|
| +
|
| + /**
|
| + * Creates a new exception with an optional error [message].
|
| + */
|
| + const IntlMessageExtractionException([this.message = ""]);
|
| +
|
| + String toString() => "IntlMessageExtractionException: $message";
|
| +}
|
|
|