Revision control

++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
++
++
++ #include <translator.h>
++ #include "metastrings.h"
++ #include "trparser.h"
++
++ #include <QtCore/QDebug>
++ #include <QtCore/QFile>
++ #include <QtCore/QString>
++ #include <QtCore/QTextStream>
++
++ #include <private/qqmljsengine_p.h>
++ #include <private/qqmljsparser_p.h>
++ #include <private/qqmljslexer_p.h>
++ #include <private/qqmljsastvisitor_p.h>
++ #include <private/qqmljsast_p.h>
++
++ #include <QCoreApplication>
++ #include <QFile>
++ #include <QFileInfo>
++ #include <QtDebug>
++ #include <QStringList>
++
++ #include <iostream>
++ #include <cstdlib>
++ #include <cctype>
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace QQmlJS;
++
++ using namespace Qt::StringLiterals;
++
++ class FindTrCalls: protected AST::Visitor
++ {
++ public:
++ FindTrCalls(Engine *engine, ConversionData &cd)
++ : engine(engine)
++ , m_cd(cd)
++ {
++ }
++
++ void operator()(Translator *translator, const QString &fileName, AST::Node *node)
++ {
++ m_todo = engine->comments();
++ m_translator = translator;
++ m_fileName = fileName;
++ m_component = QFileInfo(fileName).completeBaseName();
++ accept(node);
++
++ // process the trailing comments
++ processComments(0, /*flush*/ true);
++ }
++
++ protected:
++ using AST::Visitor::visit;
++ using AST::Visitor::endVisit;
++
++ void accept(AST::Node *node)
++ { AST::Node::accept(node, this); }
++
++ bool visit(AST::UiPragma *node) override
++ {
++ if (!node->name.isNull()) {
++ if (node->name == "Translator"_L1) {
++ m_component = node->values->value.toString();
++ }
++ }
++ return false;
++ }
++
++ void endVisit(AST::TemplateLiteral *node) override
++ {
++ do {
++ if (auto *expr = AST::cast<AST::CallExpression *>(node->expression))
++ endVisit(expr);
++ node = node->next;
++ } while (node);
++ }
++
++ void endVisit(AST::CallExpression *node) override
++ {
++ QString name;
++ AST::ExpressionNode *base = node->base;
++
++ while (base && base->kind == AST::Node::Kind_FieldMemberExpression) {
++ auto memberExpr = static_cast<AST::FieldMemberExpression *>(base);
++ name.prepend(memberExpr->name);
++ name.prepend(u'.');
++ base = memberExpr->base;
++ }
++
++ if (AST::IdentifierExpression *idExpr = AST::cast<AST::IdentifierExpression *>(base)) {
++ processComments(idExpr->identifierToken.begin());
++
++ name = idExpr->name.toString() + name;
++ const int identLineNo = idExpr->identifierToken.startLine;
++ switch (trFunctionAliasManager.trFunctionByName(name)) {
++ case TrFunctionAliasManager::Function_qsTr:
++ case TrFunctionAliasManager::Function_QT_TR_NOOP: {
++ int startOffset = node->firstSourceLocation().begin();
++ if (!node->arguments) {
++ yyMsg(identLineNo)
++ << qPrintable(QStringLiteral("%1() requires at least one argument.\n")
++ .arg(name));
++ return;
++ }
++ if (auto expr = AST::cast<AST::TemplateLiteral *>(node->arguments->expression)) {
++ if (expr->next) {
++ yyMsg(identLineNo)
++ << qPrintable(QStringLiteral("%1() template strings with "
++ "arguments are not supported for translation.\n").arg(name));
++ return;
++ }
++ }
++
++ QString source;
++ if (!createString(node->arguments->expression, &source))
++ return;
++
++ QString comment;
++ bool plural = false;
++ if (AST::ArgumentList *commentNode = node->arguments->next) {
++ if (!createString(commentNode->expression, &comment)) {
++ comment.clear(); // clear possible invalid comments
++ }
++ if (commentNode->next)
++ plural = true;
++ }
++
++ if (!m_metaStrings.sourcetext().isEmpty())
++ yyMsg(identLineNo)
++ << qPrintable("//% cannot be used with %1(). Ignoring\n"_L1.arg(name));
++
++ if (!m_metaStrings.label().isEmpty())
++ yyMsg(identLineNo) << qPrintable(
++ "labels cannot be used with text-based translation. Ignoring\n"_L1);
++
++ int endOffset = node->lastSourceLocation().end();
++ TranslatorMessage msg(m_component, transcode(source), comment, QString(),
++ m_fileName, node->firstSourceLocation().startLine,
++ QStringList(), TranslatorMessage::Unfinished, plural);
++ msg.setExtraComment(transcode(m_metaStrings.extracomment().simplified()));
++ msg.setId(m_metaStrings.msgid());
++ msg.setExtras(m_metaStrings.extra());
++ msg.setStartOffset(startOffset);
++ msg.setEndOffset(endOffset);
++ m_translator->extend(msg, m_cd);
++ consumeComment();
++ break; }
++ case TrFunctionAliasManager::Function_qsTranslate:
++ case TrFunctionAliasManager::Function_QT_TRANSLATE_NOOP: {
++ int startOffset = node->firstSourceLocation().begin();
++ if (! (node->arguments && node->arguments->next)) {
++ yyMsg(identLineNo) << qPrintable(QStringLiteral("%1() requires at least two arguments.\n").arg(name));
++ return;
++ }
++
++ QString context;
++ if (!createString(node->arguments->expression, &context))
++ return;
++
++ AST::ArgumentList *sourceNode = node->arguments->next; // we know that it is a valid pointer.
++
++ QString source;
++ if (!createString(sourceNode->expression, &source))
++ return;
++
++ if (!m_metaStrings.sourcetext().isEmpty())
++ yyMsg(identLineNo) << qPrintable(QStringLiteral("//% cannot be used with %1(). Ignoring\n").arg(name));
++
++ if (!m_metaStrings.label().isEmpty())
++ yyMsg(identLineNo) << qPrintable(
++ "labels cannot be used with text-based translation. Ignoring\n"_L1);
++
++ QString comment;
++ bool plural = false;
++ if (AST::ArgumentList *commentNode = sourceNode->next) {
++ if (!createString(commentNode->expression, &comment)) {
++ comment.clear(); // clear possible invalid comments
++ }
++
++ if (commentNode->next)
++ plural = true;
++ }
++ int endOffset = node->lastSourceLocation().end();
++ TranslatorMessage msg(context, transcode(source), comment, QString(), m_fileName,
++ node->firstSourceLocation().startLine, QStringList(),
++ TranslatorMessage::Unfinished, plural);
++ msg.setExtraComment(transcode(m_metaStrings.extracomment().simplified()));
++ msg.setId(m_metaStrings.msgid());
++ msg.setExtras(m_metaStrings.extra());
++ msg.setStartOffset(startOffset);
++ msg.setEndOffset(endOffset);
++ m_translator->extend(msg, m_cd);
++ consumeComment();
++ break; }
++ case TrFunctionAliasManager::Function_qsTrId:
++ case TrFunctionAliasManager::Function_QT_TRID_NOOP: {
++ if (!node->arguments) {
++ yyMsg(identLineNo) << qPrintable(QStringLiteral("%1() requires at least one argument.\n").arg(name));
++ return;
++ }
++ int startOffset = node->firstSourceLocation().begin();
++
++ QString id;
++ if (!createString(node->arguments->expression, &id))
++ return;
++
++ if (!m_metaStrings.msgid().isEmpty()) {
++ yyMsg(identLineNo) << qPrintable(QStringLiteral("//= cannot be used with %1(). Ignoring\n").arg(name));
++ return;
++ }
++
++ bool plural = node->arguments->next;
++ int endOffset = node->lastSourceLocation().end();
++ TranslatorMessage msg(QString(), transcode(m_metaStrings.sourcetext()), QString(),
++ QString(), m_fileName, node->firstSourceLocation().startLine,
++ QStringList(), TranslatorMessage::Unfinished, plural);
++ msg.setExtraComment(transcode(m_metaStrings.extracomment().simplified()));
++ msg.setId(id);
++ msg.setLabel(m_metaStrings.label());
++ msg.setExtras(m_metaStrings.extra());
++ msg.setStartOffset(startOffset);
++ msg.setEndOffset(endOffset);
++ m_translator->extend(msg, m_cd);
++ consumeComment();
++ break; }
++ }
++ }
++ }
++
++ void postVisit(AST::Node *node) override;
++
++ private:
++ std::ostream &yyMsg(int line)
++ {
++ return std::cerr << qPrintable(m_fileName) << ':' << line << ": ";
++ }
++
++ void throwRecursionDepthError() final
++ {
++ std::cerr << qPrintable(m_fileName) << ": "
++ << "Maximum statement or expression depth exceeded";
++ }
++
++
++ void processComments(quint32 offset, bool flush = false);
++ void processComment(const SourceLocation &loc);
++ void consumeComment();
++
++ bool createString(AST::ExpressionNode *ast, QString *out)
++ {
++ if (AST::StringLiteral *literal = AST::cast<AST::StringLiteral *>(ast)) {
++ out->append(literal->value);
++ return true;
++ } else if (AST::BinaryExpression *binop = AST::cast<AST::BinaryExpression *>(ast)) {
++ if (binop->op == QSOperator::Add && createString(binop->left, out)) {
++ if (createString(binop->right, out))
++ return true;
++ }
++ } else if (AST::TemplateLiteral *templit = AST::cast<AST::TemplateLiteral *>(ast)) {
++ out->append(templit->value);
++ return true;
++ }
++
++ return false;
++ }
++
++ Engine *engine;
++ Translator *m_translator;
++ ConversionData &m_cd;
++ QString m_fileName;
++ QString m_component;
++
++ // comments
++ MetaStrings m_metaStrings;
++ QString trcontext;
++ QList<SourceLocation> m_todo;
++ };
++
++ QString createErrorString(const QString &filename, const QString &code, Parser &parser)
++ {
++ // print out error
++ QStringList lines = code.split(u'\n');
++ lines.append("\n"_L1); // sentinel.
++ QString errorString;
++
++ const auto messages = parser.diagnosticMessages();
++ for (const DiagnosticMessage &m : messages) {
++
++ if (m.isWarning())
++ continue;
++
++ const int line = m.loc.startLine;
++ const int column = m.loc.startColumn;
++ QString error = filename + u':' + QString::number(line) + u':' + QString::number(column)
++ + ": error: "_L1 + m.message + u'\n';
++
++ const QString textLine = lines.at(line > 0 ? line - 1 : 0);
++ error += textLine + u'\n';
++ for (int i = 0, end = qMin(column > 0 ? column - 1 : 0, textLine.size()); i < end; ++i) {
++ const QChar ch = textLine.at(i);
++ if (ch.isSpace())
++ error += ch;
++ else
++ error += u' ';
++ }
++ error += "^\n"_L1;
++ errorString += error;
++ }
++ return errorString;
++ }
++
++ void FindTrCalls::postVisit(AST::Node *node)
++ {
++ if (node->statementCast() != 0 || node->uiObjectMemberCast()) {
++ processComments(node->lastSourceLocation().end());
++
++ if (m_metaStrings.hasData()) {
++ yyMsg(node->lastSourceLocation().startLine) << "Discarding unconsumed meta data\n";
++ consumeComment();
++ }
++ }
++ }
++
++ void FindTrCalls::processComments(quint32 offset, bool flush)
++ {
++ for (; !m_todo.isEmpty(); m_todo.removeFirst()) {
++ SourceLocation loc = m_todo.first();
++ if (! flush && (loc.begin() >= offset))
++ break;
++
++ processComment(loc);
++ }
++ }
++
++ void FindTrCalls::consumeComment()
++ {
++ // keep the current `trcontext'
++ m_metaStrings.clear();
++ }
++
++ void FindTrCalls::processComment(const SourceLocation &loc)
++ {
++ if (!loc.length)
++ return;
++
++ auto commentStr = QString(engine->midRef(loc.begin(), loc.length));
++
++ if (!m_metaStrings.parse(commentStr)) {
++ yyMsg(loc.startLine) << m_metaStrings.popError().toStdString();
++ return;
++ }
++
++ if (!m_metaStrings.resolveLabel(m_fileName, m_component))
++ yyMsg(loc.startLine) << m_metaStrings.popError().toStdString();
++
++ if (m_metaStrings.magicComment()) {
++ auto [context, comment] = *m_metaStrings.magicComment();
++ TranslatorMessage msg(transcode(context), QString(), transcode(comment), QString(),
++ m_fileName, loc.startLine, QStringList(), TranslatorMessage::Finished,
++ false);
++ msg.setExtraComment(transcode(m_metaStrings.extracomment().simplified()));
++ m_translator->append(msg);
++ m_translator->setExtras(m_metaStrings.extra());
++ m_metaStrings.clear();
++ }
++ }
++
++ class HasDirectives: public Directives
++ {
++ public:
++ HasDirectives(Lexer *lexer)
++ : lexer(lexer)
++ , directives(0)
++ {
++ }
++
++ bool operator()() const { return directives != 0; }
++ int end() const { return lastOffset; }
++
++ void pragmaLibrary() override { consumeDirective(); }
++ void importFile(const QString &, const QString &, int, int) override { consumeDirective(); }
++ void importModule(const QString &, const QString &, const QString &, int, int) override { consumeDirective(); }
++
++ private:
++ void consumeDirective()
++ {
++ ++directives;
++ lastOffset = lexer->tokenOffset() + lexer->tokenLength();
++ }
++
++ private:
++ Lexer *lexer;
++ int directives;
++ int lastOffset;
++ };
++
++ enum CodeType {
++ QMLCode,
++ JSCode,
++ MJSCode,
++ };
++
++ static bool load(Translator &translator, const QString &filename, ConversionData &cd, CodeType mode)
++ {
++ cd.m_sourceFileName = filename;
++ QFile file(filename);
++ if (!file.open(QIODevice::ReadOnly)) {
++ cd.appendError(QStringLiteral("Cannot open %1: %2").arg(filename, file.errorString()));
++ return false;
++ }
++
++ QString code;
++ if (mode != QMLCode) {
++ code = QTextStream(&file).readAll();
++ } else {
++ QTextStream ts(&file);
++ code = ts.readAll();
++ }
++
++ Engine driver;
++ Parser parser(&driver);
++
++ Lexer lexer(&driver);
++ lexer.setCode(code, /*line = */ 1, mode == QMLCode);
++ driver.setLexer(&lexer);
++
++ bool rc;
++ if (mode == QMLCode)
++ rc = parser.parse();
++ else if (mode == JSCode)
++ rc = parser.parseProgram();
++ else
++ rc = parser.parseModule();
++
++ if (rc) {
++ FindTrCalls trCalls(&driver, cd);
++
++ //find all tr calls in the code
++ trCalls(&translator, filename, parser.rootNode());
++ } else {
++ QString error = createErrorString(filename, code, parser);
++ cd.appendError(error);
++ return false;
++ }
++ return true;
++ }
++
++ bool loadQml(Translator &translator, const QString &filename, ConversionData &cd)
++ {
++ return load(translator, filename, cd, /*qmlMode=*/ QMLCode);
++ }
++
++ bool loadQScript(Translator &translator, const QString &filename, ConversionData &cd)
++ {
++ return load(translator, filename, cd, /*qmlMode=*/ JSCode);
++ }
++
++ bool loadJSModule(Translator &translator, const QString &filename, ConversionData &cd)
++ {
++ return load(translator, filename, cd, /*qmlMode=*/ MJSCode);
++ }
++
++ QT_END_NAMESPACE