1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the Qt Linguist of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21 ** included in the packaging of this file. Please review the following
22 ** information to ensure the GNU General Public License requirements will
23 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24 **
25 ** $QT_END_LICENSE$
26 **
27 ****************************************************************************/
28 
29 #include "lupdate.h"
30 
31 #include <translator.h>
32 
33 #include <QtCore/QDebug>
34 #include <QtCore/QFile>
35 #include <QtCore/QString>
36 
37 #include <private/qqmljsengine_p.h>
38 #include <private/qqmljsparser_p.h>
39 #include <private/qqmljslexer_p.h>
40 #include <private/qqmljsastvisitor_p.h>
41 #include <private/qqmljsast_p.h>
42 #include <private/qqmlapiversion_p.h>
43 
44 #include <QCoreApplication>
45 #include <QFile>
46 #include <QFileInfo>
47 #include <QtDebug>
48 #include <QStringList>
49 
50 #include <iostream>
51 #include <cstdlib>
52 #include <cctype>
53 
54 QT_BEGIN_NAMESPACE
55 
56 #if Q_QML_PRIVATE_API_VERSION < 8
57 namespace QQmlJS {
58     using SourceLocation = AST::SourceLocation;
59 }
60 #endif
61 
62 using namespace QQmlJS;
63 
64 static QString MagicComment(QLatin1String("TRANSLATOR"));
65 
66 class FindTrCalls: protected AST::Visitor
67 {
68 public:
FindTrCalls(Engine * engine,ConversionData & cd)69     FindTrCalls(Engine *engine, ConversionData &cd)
70         : engine(engine)
71         , m_cd(cd)
72     {
73     }
74 
operator ()(Translator * translator,const QString & fileName,AST::Node * node)75     void operator()(Translator *translator, const QString &fileName, AST::Node *node)
76     {
77         m_todo = engine->comments();
78         m_translator = translator;
79         m_fileName = fileName;
80         m_component = QFileInfo(fileName).completeBaseName();
81         accept(node);
82 
83         // process the trailing comments
84         processComments(0, /*flush*/ true);
85     }
86 
87 protected:
88     using AST::Visitor::visit;
89     using AST::Visitor::endVisit;
90 
accept(AST::Node * node)91     void accept(AST::Node *node)
92     { AST::Node::acceptChild(node, this); }
93 
endVisit(AST::CallExpression * node)94     void endVisit(AST::CallExpression *node)
95     {
96         QString name;
97         AST::ExpressionNode *base = node->base;
98 
99         while (base && base->kind == AST::Node::Kind_FieldMemberExpression) {
100             auto memberExpr = static_cast<AST::FieldMemberExpression *>(base);
101             name.prepend(memberExpr->name);
102             name.prepend(QLatin1Char('.'));
103             base = memberExpr->base;
104         }
105 
106         if (AST::IdentifierExpression *idExpr = AST::cast<AST::IdentifierExpression *>(base)) {
107             processComments(idExpr->identifierToken.begin());
108 
109             name = idExpr->name.toString() + name;
110             const int identLineNo = idExpr->identifierToken.startLine;
111             switch (trFunctionAliasManager.trFunctionByName(name)) {
112             case TrFunctionAliasManager::Function_qsTr:
113             case TrFunctionAliasManager::Function_QT_TR_NOOP: {
114                 if (!node->arguments) {
115                     yyMsg(identLineNo) << qPrintable(LU::tr("%1() requires at least one argument.\n").arg(name));
116                     return;
117                 }
118                 if (AST::cast<AST::TemplateLiteral *>(node->arguments->expression)) {
119                     yyMsg(identLineNo) << qPrintable(LU::tr("%1() cannot be used with template literals. Ignoring\n").arg(name));
120                     return;
121                 }
122 
123                 QString source;
124                 if (!createString(node->arguments->expression, &source))
125                     return;
126 
127                 QString comment;
128                 bool plural = false;
129                 if (AST::ArgumentList *commentNode = node->arguments->next) {
130                     if (!createString(commentNode->expression, &comment)) {
131                         comment.clear(); // clear possible invalid comments
132                     }
133                     if (commentNode->next)
134                         plural = true;
135                 }
136 
137                 if (!sourcetext.isEmpty())
138                     yyMsg(identLineNo) << qPrintable(LU::tr("//% cannot be used with %1(). Ignoring\n").arg(name));
139 
140                 TranslatorMessage msg(m_component, ParserTool::transcode(source),
141                     comment, QString(), m_fileName,
142                     node->firstSourceLocation().startLine, QStringList(),
143                     TranslatorMessage::Unfinished, plural);
144                 msg.setExtraComment(ParserTool::transcode(extracomment.simplified()));
145                 msg.setId(msgid);
146                 msg.setExtras(extra);
147                 m_translator->extend(msg, m_cd);
148                 consumeComment();
149                 break; }
150             case TrFunctionAliasManager::Function_qsTranslate:
151             case TrFunctionAliasManager::Function_QT_TRANSLATE_NOOP: {
152                 if (! (node->arguments && node->arguments->next)) {
153                     yyMsg(identLineNo) << qPrintable(LU::tr("%1() requires at least two arguments.\n").arg(name));
154                     return;
155                 }
156 
157                 QString context;
158                 if (!createString(node->arguments->expression, &context))
159                     return;
160 
161                 AST::ArgumentList *sourceNode = node->arguments->next; // we know that it is a valid pointer.
162 
163                 QString source;
164                 if (!createString(sourceNode->expression, &source))
165                     return;
166 
167                 if (!sourcetext.isEmpty())
168                     yyMsg(identLineNo) << qPrintable(LU::tr("//% cannot be used with %1(). Ignoring\n").arg(name));
169 
170                 QString comment;
171                 bool plural = false;
172                 if (AST::ArgumentList *commentNode = sourceNode->next) {
173                     if (!createString(commentNode->expression, &comment)) {
174                         comment.clear(); // clear possible invalid comments
175                     }
176 
177                     if (commentNode->next)
178                         plural = true;
179                 }
180 
181                 TranslatorMessage msg(context, ParserTool::transcode(source),
182                     comment, QString(), m_fileName,
183                     node->firstSourceLocation().startLine, QStringList(),
184                     TranslatorMessage::Unfinished, plural);
185                 msg.setExtraComment(ParserTool::transcode(extracomment.simplified()));
186                 msg.setId(msgid);
187                 msg.setExtras(extra);
188                 m_translator->extend(msg, m_cd);
189                 consumeComment();
190                 break; }
191             case TrFunctionAliasManager::Function_qsTrId:
192             case TrFunctionAliasManager::Function_QT_TRID_NOOP: {
193                 if (!node->arguments) {
194                     yyMsg(identLineNo) << qPrintable(LU::tr("%1() requires at least one argument.\n").arg(name));
195                     return;
196                 }
197 
198                 QString id;
199                 if (!createString(node->arguments->expression, &id))
200                     return;
201 
202                 if (!msgid.isEmpty()) {
203                     yyMsg(identLineNo) << qPrintable(LU::tr("//= cannot be used with %1(). Ignoring\n").arg(name));
204                     return;
205                 }
206 
207                 bool plural = node->arguments->next;
208 
209                 TranslatorMessage msg(QString(), ParserTool::transcode(sourcetext),
210                     QString(), QString(), m_fileName,
211                     node->firstSourceLocation().startLine, QStringList(),
212                     TranslatorMessage::Unfinished, plural);
213                 msg.setExtraComment(ParserTool::transcode(extracomment.simplified()));
214                 msg.setId(id);
215                 msg.setExtras(extra);
216                 m_translator->extend(msg, m_cd);
217                 consumeComment();
218                 break; }
219             }
220         }
221     }
222 
223     virtual void postVisit(AST::Node *node);
224 
225 private:
yyMsg(int line)226     std::ostream &yyMsg(int line)
227     {
228         return std::cerr << qPrintable(m_fileName) << ':' << line << ": ";
229     }
230 
throwRecursionDepthError()231     void throwRecursionDepthError() final
232     {
233         std::cerr << qPrintable(m_fileName) << ": "
234                   << qPrintable(LU::tr("Maximum statement or expression depth exceeded"));
235     }
236 
237 
238     void processComments(quint32 offset, bool flush = false);
239     void processComment(const SourceLocation &loc);
240     void consumeComment();
241 
createString(AST::ExpressionNode * ast,QString * out)242     bool createString(AST::ExpressionNode *ast, QString *out)
243     {
244         if (AST::StringLiteral *literal = AST::cast<AST::StringLiteral *>(ast)) {
245             out->append(literal->value);
246             return true;
247         } else if (AST::BinaryExpression *binop = AST::cast<AST::BinaryExpression *>(ast)) {
248             if (binop->op == QSOperator::Add && createString(binop->left, out)) {
249                 if (createString(binop->right, out))
250                     return true;
251             }
252         }
253 
254         return false;
255     }
256 
257     Engine *engine;
258     Translator *m_translator;
259     ConversionData &m_cd;
260     QString m_fileName;
261     QString m_component;
262 
263     // comments
264     QString extracomment;
265     QString msgid;
266     TranslatorMessage::ExtraData extra;
267     QString sourcetext;
268     QString trcontext;
269     QList<SourceLocation> m_todo;
270 };
271 
createErrorString(const QString & filename,const QString & code,Parser & parser)272 QString createErrorString(const QString &filename, const QString &code, Parser &parser)
273 {
274     // print out error
275     QStringList lines = code.split(QLatin1Char('\n'));
276     lines.append(QLatin1String("\n")); // sentinel.
277     QString errorString;
278 
279     foreach (const DiagnosticMessage &m, parser.diagnosticMessages()) {
280 
281         if (m.isWarning())
282             continue;
283 
284 #if Q_QML_PRIVATE_API_VERSION >= 8
285         const int line = m.loc.startLine;
286         const int column = m.loc.startColumn;
287 #else
288         const int line = m.line;
289         const int column = m.column;
290 #endif
291         QString error = filename + QLatin1Char(':')
292                         + QString::number(line) + QLatin1Char(':') + QString::number(column)
293                         + QLatin1String(": error: ") + m.message + QLatin1Char('\n');
294 
295         const QString textLine = lines.at(line > 0 ? line - 1 : 0);
296         error += textLine + QLatin1Char('\n');
297         for (int i = 0, end = qMin(column > 0 ? column - 1 : 0, textLine.length()); i < end; ++i) {
298             const QChar ch = textLine.at(i);
299             if (ch.isSpace())
300                 error += ch;
301             else
302                 error += QLatin1Char(' ');
303         }
304         error += QLatin1String("^\n");
305         errorString += error;
306     }
307     return errorString;
308 }
309 
postVisit(AST::Node * node)310 void FindTrCalls::postVisit(AST::Node *node)
311 {
312     if (node->statementCast() != 0 || node->uiObjectMemberCast()) {
313         processComments(node->lastSourceLocation().end());
314 
315         if (!sourcetext.isEmpty() || !extracomment.isEmpty() || !msgid.isEmpty() || !extra.isEmpty()) {
316             yyMsg(node->lastSourceLocation().startLine) << qPrintable(LU::tr("Discarding unconsumed meta data\n"));
317             consumeComment();
318         }
319     }
320 }
321 
processComments(quint32 offset,bool flush)322 void FindTrCalls::processComments(quint32 offset, bool flush)
323 {
324     for (; !m_todo.isEmpty(); m_todo.removeFirst()) {
325         SourceLocation loc = m_todo.first();
326         if (! flush && (loc.begin() >= offset))
327             break;
328 
329         processComment(loc);
330     }
331 }
332 
consumeComment()333 void FindTrCalls::consumeComment()
334 {
335     // keep the current `trcontext'
336     extracomment.clear();
337     msgid.clear();
338     extra.clear();
339     sourcetext.clear();
340 }
341 
processComment(const SourceLocation & loc)342 void FindTrCalls::processComment(const SourceLocation &loc)
343 {
344     if (!loc.length)
345         return;
346 
347     const QStringRef commentStr = engine->midRef(loc.begin(), loc.length);
348     const QChar *chars = commentStr.constData();
349     const int length = commentStr.length();
350 
351     // Try to match the logic of the C++ parser.
352     if (*chars == QLatin1Char(':') && chars[1].isSpace()) {
353         if (!extracomment.isEmpty())
354             extracomment += QLatin1Char(' ');
355         extracomment += QString(chars+2, length-2);
356     } else if (*chars == QLatin1Char('=') && chars[1].isSpace()) {
357         msgid = QString(chars+2, length-2).simplified();
358     } else if (*chars == QLatin1Char('~') && chars[1].isSpace()) {
359         QString text = QString(chars+2, length-2).trimmed();
360         int k = text.indexOf(QLatin1Char(' '));
361         if (k > -1)
362             extra.insert(text.left(k), text.mid(k + 1).trimmed());
363     } else if (*chars == QLatin1Char('%') && chars[1].isSpace()) {
364         sourcetext.reserve(sourcetext.length() + length-2);
365         ushort *ptr = (ushort *)sourcetext.data() + sourcetext.length();
366         int p = 2, c;
367         forever {
368             if (p >= length)
369                 break;
370             c = chars[p++].unicode();
371             if (std::isspace(c))
372                 continue;
373             if (c != '"') {
374                 yyMsg(loc.startLine) << qPrintable(LU::tr("Unexpected character in meta string\n"));
375                 break;
376             }
377             forever {
378                 if (p >= length) {
379                   whoops:
380                     yyMsg(loc.startLine) << qPrintable(LU::tr("Unterminated meta string\n"));
381                     break;
382                 }
383                 c = chars[p++].unicode();
384                 if (c == '"')
385                     break;
386                 if (c == '\\') {
387                     if (p >= length)
388                         goto whoops;
389                     c = chars[p++].unicode();
390                     if (c == '\r' || c == '\n')
391                         goto whoops;
392                     *ptr++ = '\\';
393                 }
394                 *ptr++ = c;
395             }
396         }
397         sourcetext.resize(ptr - (ushort *)sourcetext.data());
398     } else {
399         int idx = 0;
400         ushort c;
401         while ((c = chars[idx].unicode()) == ' ' || c == '\t' || c == '\r' || c == '\n')
402             ++idx;
403         if (!memcmp(chars + idx, MagicComment.unicode(), MagicComment.length() * 2)) {
404             idx += MagicComment.length();
405             QString comment = QString(chars + idx, length - idx).simplified();
406             int k = comment.indexOf(QLatin1Char(' '));
407             if (k == -1) {
408                 trcontext = comment;
409             } else {
410                 trcontext = comment.left(k);
411                 comment.remove(0, k + 1);
412                 TranslatorMessage msg(
413                         trcontext, QString(),
414                         comment, QString(),
415                         m_fileName, loc.startLine, QStringList(),
416                         TranslatorMessage::Finished, /*plural=*/false);
417                 msg.setExtraComment(extracomment.simplified());
418                 extracomment.clear();
419                 m_translator->append(msg);
420                 m_translator->setExtras(extra);
421                 extra.clear();
422             }
423 
424             m_component = trcontext;
425         }
426     }
427 }
428 
429 class HasDirectives: public Directives
430 {
431 public:
HasDirectives(Lexer * lexer)432     HasDirectives(Lexer *lexer)
433         : lexer(lexer)
434         , directives(0)
435     {
436     }
437 
operator ()() const438     bool operator()() const { return directives != 0; }
end() const439     int end() const { return lastOffset; }
440 
pragmaLibrary()441     void pragmaLibrary() override { consumeDirective(); }
importFile(const QString &,const QString &,int,int)442     void importFile(const QString &, const QString &, int, int) override { consumeDirective(); }
importModule(const QString &,const QString &,const QString &,int,int)443     void importModule(const QString &, const QString &, const QString &, int, int) override { consumeDirective(); }
444 
445 private:
consumeDirective()446     void consumeDirective()
447     {
448         ++directives;
449         lastOffset = lexer->tokenOffset() + lexer->tokenLength();
450     }
451 
452 private:
453     Lexer *lexer;
454     int directives;
455     int lastOffset;
456 };
457 
load(Translator & translator,const QString & filename,ConversionData & cd,bool qmlMode)458 static bool load(Translator &translator, const QString &filename, ConversionData &cd, bool qmlMode)
459 {
460     cd.m_sourceFileName = filename;
461     QFile file(filename);
462     if (!file.open(QIODevice::ReadOnly)) {
463         cd.appendError(LU::tr("Cannot open %1: %2").arg(filename, file.errorString()));
464         return false;
465     }
466 
467     QString code;
468     if (!qmlMode) {
469         code = QTextStream(&file).readAll();
470     } else {
471         QTextStream ts(&file);
472         ts.setCodec("UTF-8");
473         ts.setAutoDetectUnicode(true);
474         code = ts.readAll();
475     }
476 
477     Engine driver;
478     Parser parser(&driver);
479 
480     Lexer lexer(&driver);
481     lexer.setCode(code, /*line = */ 1, qmlMode);
482     driver.setLexer(&lexer);
483 
484     if (qmlMode ? parser.parse() : parser.parseProgram()) {
485         FindTrCalls trCalls(&driver, cd);
486 
487         //find all tr calls in the code
488         trCalls(&translator, filename, parser.rootNode());
489     } else {
490         QString error = createErrorString(filename, code, parser);
491         cd.appendError(error);
492         return false;
493     }
494     return true;
495 }
496 
loadQml(Translator & translator,const QString & filename,ConversionData & cd)497 bool loadQml(Translator &translator, const QString &filename, ConversionData &cd)
498 {
499     return load(translator, filename, cd, /*qmlMode=*/ true);
500 }
501 
loadQScript(Translator & translator,const QString & filename,ConversionData & cd)502 bool loadQScript(Translator &translator, const QString &filename, ConversionData &cd)
503 {
504     return load(translator, filename, cd, /*qmlMode=*/ false);
505 }
506 
507 QT_END_NAMESPACE
508