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