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
54QT_BEGIN_NAMESPACE
55
56#if Q_QML_PRIVATE_API_VERSION < 8
57namespace QQmlJS {
58 using SourceLocation = AST::SourceLocation;
59}
60#endif
61
62using namespace QQmlJS;
63
64static QString MagicComment(QLatin1String("TRANSLATOR"));
65
66class FindTrCalls: protected AST::Visitor
67{
68public:
69 FindTrCalls(Engine *engine, ConversionData &cd)
70 : engine(engine)
71 , m_cd(cd)
72 {
73 }
74
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(offset: 0, /*flush*/ true);
85 }
86
87protected:
88 using AST::Visitor::visit;
89 using AST::Visitor::endVisit;
90
91 void accept(AST::Node *node)
92 { AST::Node::acceptChild(node, visitor: this); }
93
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(s: memberExpr->name);
102 name.prepend(c: QLatin1Char('.'));
103 base = memberExpr->base;
104 }
105
106 if (AST::IdentifierExpression *idExpr = AST::cast<AST::IdentifierExpression *>(ast: base)) {
107 processComments(offset: idExpr->identifierToken.begin());
108
109 name = idExpr->name.toString() + name;
110 const int identLineNo = idExpr->identifierToken.startLine;
111 switch (trFunctionAliasManager.trFunctionByName(trFunctionName: name)) {
112 case TrFunctionAliasManager::Function_qsTr:
113 case TrFunctionAliasManager::Function_QT_TR_NOOP: {
114 if (!node->arguments) {
115 yyMsg(line: identLineNo) << qPrintable(LU::tr("%1() requires at least one argument.\n").arg(name));
116 return;
117 }
118 if (AST::cast<AST::TemplateLiteral *>(ast: node->arguments->expression)) {
119 yyMsg(line: 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(ast: node->arguments->expression, out: &source))
125 return;
126
127 QString comment;
128 bool plural = false;
129 if (AST::ArgumentList *commentNode = node->arguments->next) {
130 if (!createString(ast: commentNode->expression, out: &comment)) {
131 comment.clear(); // clear possible invalid comments
132 }
133 if (commentNode->next)
134 plural = true;
135 }
136
137 if (!sourcetext.isEmpty())
138 yyMsg(line: identLineNo) << qPrintable(LU::tr("//% cannot be used with %1(). Ignoring\n").arg(name));
139
140 TranslatorMessage msg(m_component, ParserTool::transcode(str: source),
141 comment, QString(), m_fileName,
142 node->firstSourceLocation().startLine, QStringList(),
143 TranslatorMessage::Unfinished, plural);
144 msg.setExtraComment(ParserTool::transcode(str: extracomment.simplified()));
145 msg.setId(msgid);
146 msg.setExtras(extra);
147 m_translator->extend(msg, cd&: 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(line: identLineNo) << qPrintable(LU::tr("%1() requires at least two arguments.\n").arg(name));
154 return;
155 }
156
157 QString context;
158 if (!createString(ast: node->arguments->expression, out: &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(ast: sourceNode->expression, out: &source))
165 return;
166
167 if (!sourcetext.isEmpty())
168 yyMsg(line: 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(ast: commentNode->expression, out: &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(str: source),
182 comment, QString(), m_fileName,
183 node->firstSourceLocation().startLine, QStringList(),
184 TranslatorMessage::Unfinished, plural);
185 msg.setExtraComment(ParserTool::transcode(str: extracomment.simplified()));
186 msg.setId(msgid);
187 msg.setExtras(extra);
188 m_translator->extend(msg, cd&: m_cd);
189 consumeComment();
190 break; }
191 case TrFunctionAliasManager::Function_qsTrId:
192 case TrFunctionAliasManager::Function_QT_TRID_NOOP: {
193 if (!node->arguments) {
194 yyMsg(line: identLineNo) << qPrintable(LU::tr("%1() requires at least one argument.\n").arg(name));
195 return;
196 }
197
198 QString id;
199 if (!createString(ast: node->arguments->expression, out: &id))
200 return;
201
202 if (!msgid.isEmpty()) {
203 yyMsg(line: 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(str: sourcetext),
210 QString(), QString(), m_fileName,
211 node->firstSourceLocation().startLine, QStringList(),
212 TranslatorMessage::Unfinished, plural);
213 msg.setExtraComment(ParserTool::transcode(str: extracomment.simplified()));
214 msg.setId(id);
215 msg.setExtras(extra);
216 m_translator->extend(msg, cd&: m_cd);
217 consumeComment();
218 break; }
219 }
220 }
221 }
222
223 virtual void postVisit(AST::Node *node);
224
225private:
226 std::ostream &yyMsg(int line)
227 {
228 return std::cerr << qPrintable(m_fileName) << ':' << line << ": ";
229 }
230
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
242 bool createString(AST::ExpressionNode *ast, QString *out)
243 {
244 if (AST::StringLiteral *literal = AST::cast<AST::StringLiteral *>(ast)) {
245 out->append(s: literal->value);
246 return true;
247 } else if (AST::BinaryExpression *binop = AST::cast<AST::BinaryExpression *>(ast)) {
248 if (binop->op == QSOperator::Add && createString(ast: binop->left, out)) {
249 if (createString(ast: 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
272QString createErrorString(const QString &filename, const QString &code, Parser &parser)
273{
274 // print out error
275 QStringList lines = code.split(sep: QLatin1Char('\n'));
276 lines.append(t: 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(i: line > 0 ? line - 1 : 0);
296 error += textLine + QLatin1Char('\n');
297 for (int i = 0, end = qMin(a: column > 0 ? column - 1 : 0, b: 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
310void FindTrCalls::postVisit(AST::Node *node)
311{
312 if (node->statementCast() != 0 || node->uiObjectMemberCast()) {
313 processComments(offset: node->lastSourceLocation().end());
314
315 if (!sourcetext.isEmpty() || !extracomment.isEmpty() || !msgid.isEmpty() || !extra.isEmpty()) {
316 yyMsg(line: node->lastSourceLocation().startLine) << qPrintable(LU::tr("Discarding unconsumed meta data\n"));
317 consumeComment();
318 }
319 }
320}
321
322void 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
333void FindTrCalls::consumeComment()
334{
335 // keep the current `trcontext'
336 extracomment.clear();
337 msgid.clear();
338 extra.clear();
339 sourcetext.clear();
340}
341
342void FindTrCalls::processComment(const SourceLocation &loc)
343{
344 if (!loc.length)
345 return;
346
347 const QStringRef commentStr = engine->midRef(position: loc.begin(), size: 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(c: QLatin1Char(' '));
361 if (k > -1)
362 extra.insert(akey: text.left(n: k), avalue: text.mid(position: k + 1).trimmed());
363 } else if (*chars == QLatin1Char('%') && chars[1].isSpace()) {
364 sourcetext.reserve(asize: 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(line: loc.startLine) << qPrintable(LU::tr("Unexpected character in meta string\n"));
375 break;
376 }
377 forever {
378 if (p >= length) {
379 whoops:
380 yyMsg(line: 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(size: 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(s1: chars + idx, s2: MagicComment.unicode(), n: MagicComment.length() * 2)) {
404 idx += MagicComment.length();
405 QString comment = QString(chars + idx, length - idx).simplified();
406 int k = comment.indexOf(c: QLatin1Char(' '));
407 if (k == -1) {
408 trcontext = comment;
409 } else {
410 trcontext = comment.left(n: k);
411 comment.remove(i: 0, len: 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
429class HasDirectives: public Directives
430{
431public:
432 HasDirectives(Lexer *lexer)
433 : lexer(lexer)
434 , directives(0)
435 {
436 }
437
438 bool operator()() const { return directives != 0; }
439 int end() const { return lastOffset; }
440
441 void pragmaLibrary() override { consumeDirective(); }
442 void importFile(const QString &, const QString &, int, int) override { consumeDirective(); }
443 void importModule(const QString &, const QString &, const QString &, int, int) override { consumeDirective(); }
444
445private:
446 void consumeDirective()
447 {
448 ++directives;
449 lastOffset = lexer->tokenOffset() + lexer->tokenLength();
450 }
451
452private:
453 Lexer *lexer;
454 int directives;
455 int lastOffset;
456};
457
458static 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(flags: QIODevice::ReadOnly)) {
463 cd.appendError(error: LU::tr(sourceText: "Cannot open %1: %2").arg(args: filename, args: 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 = */ lineno: 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
497bool loadQml(Translator &translator, const QString &filename, ConversionData &cd)
498{
499 return load(translator, filename, cd, /*qmlMode=*/ true);
500}
501
502bool loadQScript(Translator &translator, const QString &filename, ConversionData &cd)
503{
504 return load(translator, filename, cd, /*qmlMode=*/ false);
505}
506
507QT_END_NAMESPACE
508

source code of qttools/src/linguist/lupdate/qdeclarative.cpp