blob: fc8bd0999cec31534aef745d135ef360a0ef7c07 [file] [log] [blame]
// 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 'dart:io';
import 'package:analyzer_experimental/analyzer.dart';
import 'package:intl/src/intl_message.dart';
/**
* If this is true, print warnings for skipped messages. Otherwise, warnings
* are suppressed.
*/
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);
return visitor.messages;
}
/**
* 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 && !suppressWarnings) {
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";
}