Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
python.cpp
Go to the documentation of this file.
1// Copyright (C) 2002-2007 Detlev Offenbach <detlev@die-offenbachs.de>
2// Copyright (C) 2021 The Qt Company Ltd.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
4
5#include <translator.h>
6#include "trparser.h"
7#include "metastrings.h"
8
9#include <QtCore/qhash.h>
10#include <QtCore/qlist.h>
11#include <QtCore/qstring.h>
12#include <QtCore/qtextstream.h>
13#include <QtCore/qstack.h>
14
15#include <cctype>
16#include <cerrno>
17#include <cstdio>
18#include <cstring>
19
20using namespace Qt::StringLiterals;
21
22QT_BEGIN_NAMESPACE
23
24class PythonParser
25{
26
27 enum Token {
28 Tok_Eof,
29 Tok_class,
30 Tok_def,
31 Tok_return,
32 Tok_tr,
33 Tok_trUtf8,
34 Tok_translate,
35 Tok_Ident,
36 Tok_Dot,
37 Tok_String,
38 Tok_LeftParen,
39 Tok_RightParen,
40 Tok_Comma,
41 Tok_None,
42 Tok_Integer
43 };
44
45 enum class StringType { NoString, String, FormatString, RawString };
46
47public:
48 PythonParser(Translator &translator, const QString &fileName, bool &error, ConversionData &cd)
49 : tor(translator), m_cd(cd)
50 {
51#ifdef Q_CC_MSVC
52 const auto *fileNameC = reinterpret_cast<const wchar_t *>(fileName.utf16());
53 error = _wfopen_s(&yyInFile, fileNameC, L"r") != 0;
54#else
55 const QByteArray fileNameC = QFile::encodeName(fileName);
56 yyInFile = std::fopen(fileNameC.constData(), "r");
57 error = yyInFile == nullptr;
58#endif
59 if (!error)
60 startTokenizer(fileName);
61 }
62
63 /*
64 Accomplishes a very easy task: It finds all strings inside a tr() or translate()
65 call, and possibly finds out the context of the call. It supports
66 three cases:
67 (1) the context is specified, as in FunnyDialog.tr("Hello") or
68 translate("FunnyDialog", "Hello");
69 (2) the call appears within an inlined function;
70 (3) the call appears within a function defined outside the class definition.
71 */
72 void parse(const QByteArray &initialContext = {}, const QByteArray &defaultContext = {})
73 {
74 QByteArray context;
75 QByteArray text;
76 QByteArray comment;
77 QByteArray prefix;
78
79 yyTok = getToken();
80 while (yyTok != Tok_Eof) {
81
82 switch (yyTok) {
83 case Tok_class: {
84 if (yyIndentationSize < 0 && yyContinuousSpaceCount > 0)
85 yyIndentationSize = yyContinuousSpaceCount; // First indented "class"
86 const int indent =
87 yyIndentationSize > 0 ? yyContinuousSpaceCount / yyIndentationSize : 0;
88 while (!yyContextStack.isEmpty() && yyContextStack.top().second >= indent)
89 yyContextStack.pop();
90 yyTok = getToken();
91 yyContextStack.push({ yyIdent, indent });
92 yyTok = getToken();
93 } break;
94 case Tok_def:
95 if (yyIndentationSize < 0 && yyContinuousSpaceCount > 0)
96 yyIndentationSize = yyContinuousSpaceCount; // First indented "def"
97 if (!yyContextStack.isEmpty()) {
98 // Pop classes if the function is further outdented than the class on the top
99 // (end of a nested class).
100 const int classIndent = yyIndentationSize > 0
101 ? yyContinuousSpaceCount / yyIndentationSize - 1
102 : 0;
103 while (!yyContextStack.isEmpty() && yyContextStack.top().second > classIndent)
104 yyContextStack.pop();
105 }
106 yyTok = getToken();
107 break;
108 case Tok_tr:
109 case Tok_trUtf8: {
110 yyTok = getToken();
111 const int lineNo = yyCurLineNo;
112 if (match(Tok_LeftParen) && matchString(&text)) {
113 comment.clear();
114 bool plural = false;
115
116 MetaStrings metaBackup = std::move(metaStrings);
117
118 if (match(Tok_RightParen)) {
119 // There is no comment or plural arguments.
120 } else if (match(Tok_Comma) && matchStringOrNone(&comment)) {
121 // There is a comment argument.
122 if (match(Tok_RightParen)) {
123 // There is no plural argument.
124 } else if (match(Tok_Comma)) {
125 // There is a plural argument.
126 plural = true;
127 }
128 }
129
130 if (prefix.isEmpty())
131 context = defaultContext;
132 else if (prefix == "self")
133 context = yyContextStack.isEmpty() ? initialContext
134 : yyContextStack.top().first;
135 else
136 context = prefix;
137
138 prefix.clear();
139 TranslatorMessage message(QString::fromUtf8(context), QString::fromUtf8(text),
140 QString::fromUtf8(comment), {}, yyFileName, lineNo,
141 {}, TranslatorMessage::Unfinished, plural);
142 setMessageParameters(&message, metaBackup);
143 tor.extend(message, m_cd);
144 }
145 } break;
146 case Tok_translate: {
147 bool plural{};
148 const int lineNo = yyCurLineNo;
149 MetaStrings metaBackup = std::move(metaStrings);
150 if (parseTranslate(&text, &context, &comment, &plural)) {
151 TranslatorMessage message(QString::fromUtf8(context), QString::fromUtf8(text),
152 QString::fromUtf8(comment), {}, yyFileName, lineNo,
153 {}, TranslatorMessage::Unfinished, plural);
154 setMessageParameters(&message, metaBackup);
155 tor.extend(message, m_cd);
156 } else {
157 metaStrings = std::move(metaBackup);
158 }
159 } break;
160 case Tok_Ident:
161 if (!prefix.isEmpty())
162 prefix += '.';
163 prefix += yyIdent;
164 yyTok = getToken();
165 if (yyTok != Tok_Dot)
166 prefix.clear();
167 break;
168 default:
169 yyTok = getToken();
170 }
171 }
172
173 if (yyParenDepth != 0) {
174 qWarning("%s: Unbalanced parentheses in Python code", qPrintable(yyFileName));
175 }
176 }
177
178 ~PythonParser() { std::fclose(yyInFile); }
179
180private:
181 QHash<QByteArray, Token> fillTokens()
182 {
183 QHash<QByteArray, Token> tokens = { { "None", Tok_None }, { "class", Tok_class },
184 { "def", Tok_def }, { "return", Tok_return },
185 { "__tr", Tok_tr }, // Legacy?
186 { "__trUtf8", Tok_trUtf8 } };
187
188 const auto &nameMap = trFunctionAliasManager.nameToTrFunctionMap();
189 for (auto it = nameMap.cbegin(), end = nameMap.cend(); it != end; ++it) {
190 switch (it.value()) {
191 case TrFunctionAliasManager::Function_tr:
192 case TrFunctionAliasManager::Function_QT_TR_NOOP:
193 tokens.insert(it.key().toUtf8(), Tok_tr);
194 break;
195 case TrFunctionAliasManager::Function_trUtf8:
196 tokens.insert(it.key().toUtf8(), Tok_trUtf8);
197 break;
198 case TrFunctionAliasManager::Function_translate:
199 case TrFunctionAliasManager::Function_QT_TRANSLATE_NOOP:
200 // QTranslator::findMessage() has the same parameters as QApplication::translate().
201 case TrFunctionAliasManager::Function_findMessage:
202 tokens.insert(it.key().toUtf8(), Tok_translate);
203 break;
204 default:
205 break;
206 }
207 }
208 return tokens;
209 }
210
211 QHash<QByteArray, Token> &getTokens()
212 {
213 static QHash<QByteArray, Token> tokens = fillTokens();
214 return tokens;
215 }
216
217 int getChar()
218 {
219 int c;
220
221 if (buf < 0) {
222 c = getc(yyInFile);
223 } else {
224 c = buf;
225 buf = -1;
226 }
227 if (c == '\n') {
228 yyCurLineNo++;
229 yyCountingIndentation = true;
230 yyContinuousSpaceCount = 0;
231 } else if (yyCountingIndentation && (c == 32 || c == 9)) {
232 yyContinuousSpaceCount++;
233 } else {
234 yyCountingIndentation = false;
235 }
236 return c;
237 }
238
239 int peekChar()
240 {
241 int c = getc(yyInFile);
242 buf = c;
243 return c;
244 }
245
246 void startTokenizer(const QString &fileName)
247 {
248 yyInPos = 0;
249 buf = -1;
250
251 yyFileName = fileName;
252 yyCh = getChar();
253 yyParenDepth = 0;
254 yyCurLineNo = 1;
255
256 yyIndentationSize = -1;
257 yyContinuousSpaceCount = 0;
258 yyContextStack.clear();
259 }
260
261 bool parseStringEscape(int quoteChar, StringType stringType)
262 {
263 static const char tab[] = "abfnrtv";
264 static const char backTab[] = "\a\b\f\n\r\t\v";
265
266 yyCh = getChar();
267 if (yyCh == EOF)
268 return false;
269
270 if (stringType == StringType::RawString) {
271 if (yyCh != quoteChar) // Only quotes can be escaped in raw strings
272 yyString[yyStringLen++] = '\\';
273 yyString[yyStringLen++] = yyCh;
274 yyCh = getChar();
275 return true;
276 }
277
278 if (yyCh == 'x' || yyCh == 'u' || yyCh == 'U') {
279 qsizetype maxSize = 2; // \x
280 if (yyCh == 'u')
281 maxSize = 4;
282 else if (yyCh == 'U')
283 maxSize = 8;
284
285 QByteArray hex;
286 yyCh = getChar();
287 if (yyCh == EOF)
288 return false;
289
290 while (maxSize-- && std::isxdigit(yyCh)) {
291 hex += char(yyCh);
292 yyCh = getChar();
293 if (yyCh == EOF)
294 return false;
295 }
296 uint n;
297#ifdef Q_CC_MSVC
298 sscanf_s(hex.constData(), "%x", &n);
299#else
300 std::sscanf(hex.constData(), "%x", &n);
301#endif
302
303 QByteArray hexChar = QString(QChar(n)).toUtf8();
304 if (yyStringLen < sizeof(yyString) - hexChar.size())
305 for (char c : std::as_const(hexChar))
306 yyString[yyStringLen++] = c;
307 return true;
308 }
309
310 if (yyCh >= '0' && yyCh < '8') {
311 QByteArray oct;
312 int n = 0;
313 do {
314 oct += char(yyCh);
315 ++n;
316 yyCh = getChar();
317 if (yyCh == EOF)
318 return false;
319 } while (yyCh >= '0' && yyCh < '8' && n < 3);
320#ifdef Q_CC_MSVC
321 sscanf_s(oct.constData(), "%o", &n);
322#else
323 std::sscanf(oct.constData(), "%o", &n);
324#endif
325 if (yyStringLen < sizeof(yyString) - 1)
326 yyString[yyStringLen++] = char(n);
327 return true;
328 }
329
330 const char *p = std::strchr(tab, yyCh);
331 if (yyStringLen < sizeof(yyString) - 1) {
332 yyString[yyStringLen++] = p == nullptr ? char(yyCh) : backTab[p - tab];
333 }
334 yyCh = getChar();
335 return true;
336 }
337
338 Token parseString(StringType stringType = StringType::NoString)
339 {
340 int quoteChar = yyCh;
341 bool tripleQuote = false;
342 bool singleQuote = true;
343 bool in = false;
344
345 yyCh = getChar();
346
347 while (yyCh != EOF) {
348 if (singleQuote && (yyCh == '\n' || (in && yyCh == quoteChar)))
349 break;
350
351 if (yyCh == quoteChar) {
352 if (peekChar() == quoteChar) {
353 yyCh = getChar();
354 if (!tripleQuote) {
355 tripleQuote = true;
356 singleQuote = false;
357 in = true;
358 yyCh = getChar();
359 } else {
360 yyCh = getChar();
361 if (yyCh == quoteChar) {
362 tripleQuote = false;
363 break;
364 }
365 }
366 } else if (tripleQuote) {
367 if (yyStringLen < sizeof(yyString) - 1)
368 yyString[yyStringLen++] = char(yyCh);
369 yyCh = getChar();
370 continue;
371 } else {
372 break;
373 }
374 } else {
375 in = true;
376 }
377
378 if (yyCh == '\\') {
379 if (!parseStringEscape(quoteChar, stringType))
380 return Tok_Eof;
381 } else {
382 char *yStart = yyString + yyStringLen;
383 char *yp = yStart;
384 while (yyCh != EOF && (tripleQuote || yyCh != '\n') && yyCh != quoteChar
385 && yyCh != '\\') {
386 *yp++ = char(yyCh);
387 yyCh = getChar();
388 }
389 yyStringLen += yp - yStart;
390 }
391 }
392 yyString[yyStringLen] = '\0';
393
394 if (yyCh != quoteChar) {
395 printf("%c\n", yyCh);
396
397 qWarning("%s:%d: Unterminated string", qPrintable(yyFileName), yyLineNo);
398 }
399
400 if (yyCh == EOF)
401 return Tok_Eof;
402 yyCh = getChar();
403 return Tok_String;
404 }
405
406 QByteArray readLine()
407 {
408 QByteArray result;
409 while (true) {
410 yyCh = getChar();
411 if (yyCh == EOF || yyCh == '\n')
412 break;
413 result.append(char(yyCh));
414 }
415 return result;
416 }
417
418 Token getToken(StringType stringType = StringType::NoString)
419 {
420 yyIdent.clear();
421 yyStringLen = 0;
422 while (yyCh != EOF) {
423 yyLineNo = yyCurLineNo;
424
425 if (std::isalpha(yyCh) || yyCh == '_') {
426 do {
427 yyIdent.append(char(yyCh));
428 yyCh = getChar();
429 } while (std::isalnum(yyCh) || yyCh == '_');
430
431 return getTokens().value(yyIdent, Tok_Ident);
432 }
433 switch (yyCh) {
434 case '#': {
435 auto comment = QString::fromUtf8(readLine());
436 if (!metaStrings.parse(comment)) {
437 qWarning() << qPrintable(yyFileName) << ':' << yyLineNo << ": "
438 << metaStrings.popError().toStdString();
439 break;
440 }
441 if (metaStrings.magicComment()) {
442 auto [context, comment] = *metaStrings.magicComment();
443 TranslatorMessage msg(transcode(context), QString(), transcode(comment),
444 QString(), yyFileName, yyCurLineNo, QStringList(),
445 TranslatorMessage::Finished, false);
446 msg.setExtraComment(transcode(metaStrings.extracomment().simplified()));
447 tor.append(msg);
448 tor.setExtras(metaStrings.extra());
449 metaStrings.clear();
450 }
451 break;
452 }
453 case '"':
454 case '\'':
455 return parseString(stringType);
456 case '(':
457 yyParenDepth++;
458 yyCh = getChar();
459 return Tok_LeftParen;
460 case ')':
461 yyParenDepth--;
462 yyCh = getChar();
463 return Tok_RightParen;
464 case ',':
465 yyCh = getChar();
466 return Tok_Comma;
467 case '.':
468 yyCh = getChar();
469 return Tok_Dot;
470 case '0':
471 case '1':
472 case '2':
473 case '3':
474 case '4':
475 case '5':
476 case '6':
477 case '7':
478 case '8':
479 case '9': {
480 QByteArray ba;
481 ba += char(yyCh);
482 yyCh = getChar();
483 const bool hex = yyCh == 'x';
484 if (hex) {
485 ba += char(yyCh);
486 yyCh = getChar();
487 }
488 while ((hex ? std::isxdigit(yyCh) : std::isdigit(yyCh))) {
489 ba += char(yyCh);
490 yyCh = getChar();
491 }
492 bool ok;
493 auto v = ba.toLongLong(&ok);
494 Q_UNUSED(v);
495 if (ok)
496 return Tok_Integer;
497 break;
498 }
499 default:
500 yyCh = getChar();
501 }
502 }
503 return Tok_Eof;
504 }
505
506 bool match(Token t)
507 {
508 const bool matches = (yyTok == t);
509 if (matches)
510 yyTok = getToken();
511 return matches;
512 }
513
514 bool matchStringStart()
515 {
516 if (yyTok == Tok_String)
517 return true;
518 // Check for f"bla{var}" and raw strings r"bla".
519 if (yyTok == Tok_Ident && yyIdent.size() == 1) {
520 switch (yyIdent.at(0)) {
521 case 'r':
522 yyTok = getToken(StringType::RawString);
523 return yyTok == Tok_String;
524 case 'f':
525 yyTok = getToken(StringType::FormatString);
526 return yyTok == Tok_String;
527 }
528 }
529 return false;
530 }
531
532 bool matchString(QByteArray *s)
533 {
534 s->clear();
535 bool ok = false;
536 while (matchStringStart()) {
537 *s += yyString;
538 yyTok = getToken();
539 ok = true;
540 }
541 return ok;
542 }
543
544 bool matchStringOrNone(QByteArray *s)
545 {
546 bool matches = matchString(s);
547
548 if (!matches)
549 matches = match(Tok_None);
550
551 return matches;
552 }
553
554 /*
555 * Skip any expression that can return a number, which can be
556 * 1. Literal number (e.g. '11')
557 * 2. simple identifier (e.g. 'm_count')
558 * 3. simple function call (e.g. 'size()')
559 * 4. function call on an object (e.g. 'list.size()')
560 * * Other cases:
561 * size(2,4)
562 * list().size()
563 * list(a,b).size(2,4)
564 * etc. (modeled after cpp CppParser::skipExpression()).
565 */
566 bool skipExpression()
567 {
568 if (match(Tok_Integer))
569 return true;
570
571 int parenlevel = 0;
572 while (parenlevel >= 0) {
573 yyTok = getToken();
574 if (yyTok == Tok_RightParen)
575 --parenlevel;
576 else if (yyTok == Tok_LeftParen)
577 ++parenlevel;
578 else if (yyTok == Tok_Eof)
579 return false;
580 }
581 return true;
582 }
583
584 bool parseTranslate(QByteArray *text, QByteArray *context, QByteArray *comment, bool *plural)
585 {
586 text->clear();
587 context->clear();
588 comment->clear();
589 *plural = false;
590
591 yyTok = getToken();
592 if (!match(Tok_LeftParen) || !matchString(context) || !match(Tok_Comma)
593 || !matchString(text)) {
594 return false;
595 }
596
597 if (match(Tok_RightParen))
598 return true;
599
600 // not a comma or a right paren, illegal syntax
601 if (!match(Tok_Comma))
602 return false;
603
604 // python accepts trailing commas within parenthesis, so allow a comma with nothing after
605 if (match(Tok_RightParen))
606 return true;
607
608 // check for comment
609 if (!matchStringOrNone(comment))
610 return false; // not a comment, or a trailing comma... something is wrong
611
612 if (match(Tok_RightParen))
613 return true;
614
615 // not a comma or a right paren, illegal syntax
616 if (!match(Tok_Comma))
617 return false;
618
619 // python accepts trailing commas within parenthesis, so allow a comma with nothing after
620 if (match(Tok_RightParen))
621 return true;
622
623 // Must be a plural expression
624 if (!skipExpression())
625 return false;
626
627 *plural = true;
628
629 // Ignore any trailing comma here
630 match(Tok_Comma);
631
632 // This must be the end, or there are too many parameters
633 if (match(Tok_RightParen))
634 return true;
635
636 return false;
637 }
638
639 void setMessageParameters(TranslatorMessage *message, const MetaStrings &meta)
640 {
641 // PYSIDE-2863: parseTranslate() can read past the message
642 // and capture extraComments intended for the next message.
643 // Use only extraComments for the current message.
644
645 message->setExtraComment(transcode(meta.extracomment().simplified()));
646 message->setId(meta.msgid());
647 message->setExtras(meta.extra());
648 if (!meta.label().isEmpty())
649 m_cd.appendError("%1:%2: labels cannot be used with text-based translation. "
650 "Ignoring\n"_L1.arg(yyFileName)
651 .arg(yyLineNo));
652 }
653
654 QString yyFileName;
655 Token yyTok{};
656 int yyCh{};
657 QByteArray yyIdent;
658 char yyString[65536];
659 size_t yyStringLen{};
660 int yyParenDepth{};
661 int yyLineNo = 1;
662 int yyCurLineNo{};
663 // the file to read from (if reading from a file)
664 FILE *yyInFile;
665 // the string to read from and current position in the string (otherwise)
666 int yyInPos{};
667 int buf{};
668 int yyIndentationSize{};
669 int yyContinuousSpaceCount{};
670 bool yyCountingIndentation = false;
671 // (Context, indentation level) pair.
672 using ContextPair = QPair<QByteArray, int>;
673 // Stack of (Context, indentation level) pairs.
674 using ContextStack = QStack<ContextPair>;
675 ContextStack yyContextStack;
676 MetaStrings metaStrings;
677 Translator &tor;
678 ConversionData &m_cd;
679};
680
681bool loadPython(Translator &translator, const QString &fileName, ConversionData &cd)
682{
683
684 bool error = false;
685 PythonParser parser(translator, fileName, error, cd);
686 if (error) {
687 cd.appendError(QStringLiteral("Cannot open %1").arg(fileName));
688 return false;
689 }
690
691 parser.parse();
692 return true;
693}
694
695QT_END_NAMESPACE
bool loadPython(Translator &translator, const QString &fileName, ConversionData &cd)
Definition python.cpp:681