#include "log.h" #include "parseutil.h" #include <QRegularExpression> #include <QJsonDocument> #include <QJsonObject> #include <QStack> #include "lib/fex/lexer.h" #include "lib/fex/parser.h" const QRegularExpression ParseUtil::re_incScriptLabel("\\b(?<label>[\\w_][\\w\\d_]*):{1,2}"); const QRegularExpression ParseUtil::re_globalIncScriptLabel("\\b(?<label>[\\w_][\\w\\d_]*)::"); const QRegularExpression ParseUtil::re_poryScriptLabel("\\b(script)(\\((global|local)\\))?\\s*\\b(?<label>[\\w_][\\w\\d_]*)"); const QRegularExpression ParseUtil::re_globalPoryScriptLabel("\\b(script)(\\((global)\\))?\\s*\\b(?<label>[\\w_][\\w\\d_]*)"); const QRegularExpression ParseUtil::re_poryRawSection("\\b(raw)\\s*`(?<raw_script>[^`]*)"); static const QMap<QString, int> globalDefineValues = { {"FALSE", 0}, {"TRUE", 1}, {"SCHAR_MIN", SCHAR_MIN}, {"SCHAR_MAX", SCHAR_MAX}, {"CHAR_MIN", CHAR_MIN}, {"CHAR_MAX", CHAR_MAX}, {"UCHAR_MAX", UCHAR_MAX}, {"SHRT_MIN", SHRT_MIN}, {"SHRT_MAX", SHRT_MAX}, {"USHRT_MAX", USHRT_MAX}, {"INT_MIN", INT_MIN}, {"INT_MAX", INT_MAX}, {"UINT_MAX", UINT_MAX}, }; using OrderedJson = poryjson::Json; ParseUtil::ParseUtil() { } void ParseUtil::set_root(const QString &dir) { this->root = dir; } void ParseUtil::recordError(const QString &message) { this->errorMap[this->curDefine].append(message); } void ParseUtil::recordErrors(const QStringList &errors) { if (errors.isEmpty()) return; this->errorMap[this->curDefine].append(errors); } void ParseUtil::logRecordedErrors() { QStringList errors = this->errorMap.value(this->curDefine); if (errors.isEmpty()) return; QString message = QString("Failed to parse '%1':").arg(this->curDefine); for (const auto error : errors) message.append(QString("\n%1").arg(error)); logError(message); } QString ParseUtil::createErrorMessage(const QString &message, const QString &expression) { static const QRegularExpression newline("[\r\n]"); QStringList lines = this->text.split(newline); int lineNum = 0, colNum = 0; for (QString line : lines) { lineNum++; colNum = line.indexOf(expression) + 1; if (colNum) break; } return QString("%1:%2:%3: %4").arg(this->file).arg(lineNum).arg(colNum).arg(message); } QString ParseUtil::readTextFile(const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { logError(QString("Could not open '%1': ").arg(path) + file.errorString()); return QString(); } QTextStream in(&file); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) in.setCodec("UTF-8"); #endif // Qt6 defaults to UTF-8, but setCodec is renamed to setEncoding QString text = ""; while (!in.atEnd()) { text += in.readLine() + '\n'; } return text; } int ParseUtil::textFileLineCount(const QString &path) { const QString text = readTextFile(path); return text.split('\n').count() + 1; } QList<QStringList> ParseUtil::parseAsm(const QString &filename) { QList<QStringList> parsed; this->text = readTextFile(this->root + '/' + filename); const QStringList lines = removeLineComments(this->text, "@").split('\n'); for (const auto &line : lines) { const QString trimmedLine = line.trimmed(); if (trimmedLine.isEmpty()) { continue; } if (line.contains(':')) { const QString label = line.left(line.indexOf(':')); const QStringList list{ ".label", label }; // .label is not a real keyword. It's used only to make the output more regular. parsed.append(list); // There should not be anything else on the line. // gas will raise a syntax error if there is. } else { static const QRegularExpression re_spaces("\\s+"); int index = trimmedLine.indexOf(re_spaces); const QString macro = trimmedLine.left(index); static const QRegularExpression re_spacesCommaSpaces("\\s*,\\s*"); QStringList params(trimmedLine.right(trimmedLine.length() - index).trimmed().split(re_spacesCommaSpaces)); params.prepend(macro); parsed.append(params); } } return parsed; } // 'identifier' is the name of the #define to evaluate, e.g. 'FOO' in '#define FOO (BAR+1)' // 'expression' is the text of the #define to evaluate, e.g. '(BAR+1)' in '#define FOO (BAR+1)' // 'knownValues' is a pointer to a map of identifier->values for defines that have already been evaluated. // 'unevaluatedExpressions' is a pointer to a map of identifier->expressions for defines that have not been evaluated. If this map contains any // identifiers found in 'expression' then this function will be called recursively to evaluate that define first. // This function will maintain the passed maps appropriately as new #defines are evaluated. int ParseUtil::evaluateDefine(const QString &identifier, const QString &expression, QMap<QString, int> *knownValues, QMap<QString, QString> *unevaluatedExpressions) { if (unevaluatedExpressions->contains(identifier)) unevaluatedExpressions->remove(identifier); if (knownValues->contains(identifier)) return knownValues->value(identifier); QList<Token> tokens = tokenizeExpression(expression, knownValues, unevaluatedExpressions); QList<Token> postfixExpression = generatePostfix(tokens); int value = evaluatePostfix(postfixExpression); knownValues->insert(identifier, value); return value; } QList<Token> ParseUtil::tokenizeExpression(QString expression, QMap<QString, int> *knownValues, QMap<QString, QString> *unevaluatedExpressions) { QList<Token> tokens; static const QStringList tokenTypes = {"hex", "decimal", "identifier", "operator", "leftparen", "rightparen"}; static const QRegularExpression re("^(?<hex>0x[0-9a-fA-F]+)|(?<decimal>[0-9]+)|(?<identifier>[a-zA-Z_0-9]+)|(?<operator>[+\\-*\\/<>|^%]+)|(?<leftparen>\\()|(?<rightparen>\\))"); expression = expression.trimmed(); while (!expression.isEmpty()) { QRegularExpressionMatch match = re.match(expression); if (!match.hasMatch()) { logWarn(QString("Failed to tokenize expression: '%1'").arg(expression)); break; } for (QString tokenType : tokenTypes) { QString token = match.captured(tokenType); if (!token.isEmpty()) { if (tokenType == "identifier") { if (unevaluatedExpressions->contains(token)) { // This expression depends on a define we know of but haven't evaluated. Evaluate it now evaluateDefine(token, unevaluatedExpressions->value(token), knownValues, unevaluatedExpressions); } if (knownValues->contains(token)) { // Any errors encountered when this identifier was evaluated should be recorded for this expression as well. recordErrors(this->errorMap.value(token)); QString actualToken = QString("%1").arg(knownValues->value(token)); expression = expression.replace(0, token.length(), actualToken); token = actualToken; tokenType = "decimal"; } else { tokenType = "error"; QString message = QString("unknown token '%1' found in expression '%2'") .arg(token).arg(expression); recordError(createErrorMessage(message, expression)); } } else if (tokenType == "operator") { if (!Token::precedenceMap.contains(token)) { QString message = QString("unsupported postfix operator: '%1'") .arg(token); recordError(createErrorMessage(message, expression)); } } tokens.append(Token(token, tokenType)); expression = expression.remove(0, token.length()).trimmed(); break; } } } return tokens; } QMap<QString, int> Token::precedenceMap = QMap<QString, int>( { {"*", 3}, {"/", 3}, {"%", 3}, {"+", 4}, {"-", 4}, {"<<", 5}, {">>", 5}, {"&", 8}, {"^", 9}, {"|", 10} }); // Shunting-yard algorithm for generating postfix notation. // https://en.wikipedia.org/wiki/Shunting-yard_algorithm QList<Token> ParseUtil::generatePostfix(const QList<Token> &tokens) { QList<Token> output; QStack<Token> operatorStack; for (Token token : tokens) { if (token.type == TokenClass::Number) { output.append(token); } else if (token.value == "(") { operatorStack.push(token); } else if (token.value == ")") { while (!operatorStack.empty() && operatorStack.top().value != "(") { output.append(operatorStack.pop()); } if (!operatorStack.empty()) { // pop the left parenthesis token operatorStack.pop(); } else { recordError("Mismatched parentheses detected in expression!"); } } else { // token is an operator while (!operatorStack.isEmpty() && operatorStack.top().operatorPrecedence <= token.operatorPrecedence && operatorStack.top().value != "(") { output.append(operatorStack.pop()); } operatorStack.push(token); } } while (!operatorStack.isEmpty()) { if (operatorStack.top().value == "(" || operatorStack.top().value == ")") { recordError("Mismatched parentheses detected in expression!"); } else { output.append(operatorStack.pop()); } } return output; } // Evaluate postfix expression. // https://en.wikipedia.org/wiki/Reverse_Polish_notation#Postfix_evaluation_algorithm int ParseUtil::evaluatePostfix(const QList<Token> &postfix) { QStack<Token> stack; for (Token token : postfix) { if (token.type == TokenClass::Operator && stack.size() > 1) { int op2 = stack.pop().value.toInt(nullptr, 0); int op1 = stack.pop().value.toInt(nullptr, 0); int result = 0; if (token.value == "*") { result = op1 * op2; } else if (token.value == "/") { result = op1 / op2; } else if (token.value == "%") { result = op1 % op2; } else if (token.value == "+") { result = op1 + op2; } else if (token.value == "-") { result = op1 - op2; } else if (token.value == "<<") { result = op1 << op2; } else if (token.value == ">>") { result = op1 >> op2; } else if (token.value == "&") { result = op1 & op2; } else if (token.value == "^") { result = op1 ^ op2; } else if (token.value == "|") { result = op1 | op2; } stack.push(Token(QString("%1").arg(result), "decimal")); } else if (token.type != TokenClass::Error) { stack.push(token); } // else ignore errored tokens, we have already warned the user. } return stack.size() ? stack.pop().value.toInt(nullptr, 0) : 0; } QString ParseUtil::readCIncbin(const QString &filename, const QString &label) { QString path; if (label.isNull()) { return path; } this->text = readTextFile(this->root + "/" + filename); QRegularExpression re(QString( "\\b%1\\b" "\\s*\\[?\\s*\\]?\\s*=\\s*" "INCBIN_[US][0-9][0-9]?" "\\(\\s*\"([^\"]*)\"\\s*\\)").arg(label)); QRegularExpressionMatch match; qsizetype pos = this->text.indexOf(re, 0, &match); if (pos != -1) { path = match.captured(1); } return path; } QMap<QString, QString> ParseUtil::readCIncbinMulti(const QString &filepath) { QMap<QString, QString> incbinMap; this->file = filepath; this->text = readTextFile(this->root + "/" + filepath); static const QRegularExpression regex("(?<label>[A-Za-z0-9_]+)\\s*\\[?\\s*\\]?\\s*=\\s*INCBIN_[US][0-9][0-9]?\\(\\s*\\\"(?<path>[^\\\\\"]*)\\\"\\s*\\)"); QRegularExpressionMatchIterator iter = regex.globalMatch(this->text); while (iter.hasNext()) { QRegularExpressionMatch match = iter.next(); QString label = match.captured("label"); QString labelText = match.captured("path"); incbinMap[label] = labelText; } return incbinMap; } QStringList ParseUtil::readCIncbinArray(const QString &filename, const QString &label) { QStringList paths; if (label.isNull()) { return paths; } this->text = readTextFile(this->root + "/" + filename); bool found = false; QString arrayText; // Get the text starting after the label all the way to the definition's end static const QRegularExpression re_labelGroup(QString("(?<label>[A-Za-z0-9_]+)\\[([^;]*?)};"), QRegularExpression::DotMatchesEverythingOption); QRegularExpressionMatchIterator findLabelIter = re_labelGroup.globalMatch(this->text); while (findLabelIter.hasNext()) { QRegularExpressionMatch labelMatch = findLabelIter.next(); if (labelMatch.captured("label") == label) { found = true; arrayText = labelMatch.captured(2); break; } } if (!found) { return paths; } // Extract incbin paths from the array static const QRegularExpression re_incbin("INCBIN_[US][0-9][0-9]?\\(\\s*\"([^\"]*)\"\\s*\\)"); QRegularExpressionMatchIterator iter = re_incbin.globalMatch(arrayText); while (iter.hasNext()) { paths.append(iter.next().captured(1)); } return paths; } bool ParseUtil::defineNameMatchesFilter(const QString &name, const QStringList &filterList, bool useRegex) { for (auto filter : filterList) { if (useRegex) { // TODO: These QRegularExpression should probably be constructed beforehand, // otherwise we recreate them for every define we check. if (QRegularExpression(filter).match(name).hasMatch()) return true; } else if (name == filter) return true; } return false; } ParseUtil::ParsedDefines ParseUtil::readCDefines(const QString &filename, const QStringList &filterList, bool useRegex) { ParsedDefines result; this->file = filename; if (this->file.isEmpty()) { return result; } QString filepath = this->root + "/" + this->file; this->text = readTextFile(filepath); if (this->text.isNull()) { logError(QString("Failed to read C defines file: '%1'").arg(filepath)); return result; } static const QRegularExpression re_extraChars("(//.*)|(\\/+\\*+[^*]*\\*+\\/+)"); this->text.replace(re_extraChars, ""); static const QRegularExpression re_extraSpaces("(\\\\\\s+)"); this->text.replace(re_extraSpaces, ""); if (this->text.isEmpty()) return result; // Capture either the name and value of a #define, or everything between the braces of 'enum { }' static const QRegularExpression re("#define\\s+(?<defineName>\\w+)[\\s\\n][^\\S\\n]*(?<defineValue>.+)?" "|\\benum\\b[^{]*{(?<enumBody>[^}]*)}"); QRegularExpressionMatchIterator iter = re.globalMatch(this->text); while (iter.hasNext()) { QRegularExpressionMatch match = iter.next(); const QString enumBody = match.captured("enumBody"); if (!enumBody.isNull()) { // Encountered an enum, extract the elements of the enum and give each an appropriate expression int baseNum = 0; QString baseExpression = "0"; // Note: We lazily consider an enum's expression to be any characters after the assignment up until the first comma or EOL. // This would be a problem for e.g. NAME = MACRO(a, b), but we're currently unable to parse function-like macros anyway. // If this changes then the regex below needs to be updated. static const QRegularExpression re_enumElement("\\b(?<name>\\w+)\\b\\s*=?\\s*(?<expression>[^,]*)"); QRegularExpressionMatchIterator elementIter = re_enumElement.globalMatch(enumBody); while (elementIter.hasNext()) { QRegularExpressionMatch elementMatch = elementIter.next(); const QString name = elementMatch.captured("name"); QString expression = elementMatch.captured("expression"); if (expression.isEmpty()) { // enum values may use tokens that we don't know how to evaluate yet. // For now we define each element to be 1 + the previous element's expression. expression = QString("((%1)+%2)").arg(baseExpression).arg(baseNum++); } else { // This element was explicitly assigned an expression with '=', reset the bases for any subsequent elements. baseExpression = expression; baseNum = 1; } result.expressions.insert(name, expression); if (defineNameMatchesFilter(name, filterList, useRegex)) result.filteredNames.append(name); } } else { // Encountered a #define const QString name = match.captured("defineName"); result.expressions.insert(name, match.captured("defineValue")); if (defineNameMatchesFilter(name, filterList, useRegex)) result.filteredNames.append(name); } } return result; } // Read all the define names and their expressions in the specified file, then evaluate the ones matching the search text (and any they depend on). QMap<QString, int> ParseUtil::evaluateCDefines(const QString &filename, const QStringList &filterList, bool useRegex) { ParsedDefines defines = readCDefines(filename, filterList, useRegex); // Evaluate defines QMap<QString, int> filteredValues; QMap<QString, int> allValues = globalDefineValues; this->errorMap.clear(); while (!defines.filteredNames.isEmpty()) { const QString name = defines.filteredNames.takeFirst(); const QString expression = defines.expressions.take(name); if (expression == " ") continue; this->curDefine = name; filteredValues.insert(name, evaluateDefine(name, expression, &allValues, &defines.expressions)); logRecordedErrors(); // Only log errors for defines that Porymap is looking for } return filteredValues; } // Find and evaluate a specific set of defines with known names. QMap<QString, int> ParseUtil::readCDefinesByName(const QString &filename, const QStringList &names) { return evaluateCDefines(filename, names, false); } // Find and evaluate an unknown list of defines with a known name pattern. QMap<QString, int> ParseUtil::readCDefinesByRegex(const QString &filename, const QStringList ®exList) { return evaluateCDefines(filename, regexList, true); } // Find an unknown list of defines with a known name pattern. // Similar to readCDefinesByRegex, but for cases where we only need to show a list of define names. // We can skip evaluating any expressions (and by extension skip reporting any errors from this process). QStringList ParseUtil::readCDefineNames(const QString &filename, const QStringList ®exList) { return readCDefines(filename, regexList, true).filteredNames; } QStringList ParseUtil::readCArray(const QString &filename, const QString &label) { QStringList list; if (label.isNull()) { return list; } this->file = filename; this->text = readTextFile(this->root + "/" + filename); QRegularExpression re(QString(R"(\b%1\b\s*(\[?[^\]]*\])?\s*=\s*\{([^\}]*)\})").arg(label)); QRegularExpressionMatch match = re.match(this->text); if (match.hasMatch()) { QString body = match.captured(2); QStringList split = body.split(','); for (QString item : split) { item = item.trimmed(); static const QRegularExpression validChars("[^A-Za-z0-9_&()\\s]"); if (!item.contains(validChars)) list.append(item); // do not print error info here because this is called dozens of times } } return list; } QMap<QString, QStringList> ParseUtil::readCArrayMulti(const QString &filename) { QMap<QString, QStringList> map; this->file = filename; this->text = readTextFile(this->root + "/" + filename); static const QRegularExpression regex(R"((?<label>\b[A-Za-z0-9_]+\b)\s*(\[[^\]]*\])?\s*=\s*\{(?<body>[^\}]*)\})"); QRegularExpressionMatchIterator iter = regex.globalMatch(this->text); while (iter.hasNext()) { QRegularExpressionMatch match = iter.next(); QString label = match.captured("label"); QString body = match.captured("body"); QStringList list; QStringList split = body.split(','); for (QString item : split) { item = item.trimmed(); static const QRegularExpression validChars("[^A-Za-z0-9_&()\\s]"); if (!item.contains(validChars)) list.append(item); // do not print error info here because this is called dozens of times } map[label] = list; } return map; } QMap<QString, QString> ParseUtil::readNamedIndexCArray(const QString &filename, const QString &label) { this->text = readTextFile(this->root + "/" + filename); QMap<QString, QString> map; QRegularExpression re_text(QString(R"(\b%1\b\s*(\[?[^\]]*\])?\s*=\s*\{([^\}]*)\})").arg(label)); QString arrayText = re_text.match(this->text).captured(2).replace(QRegularExpression("\\s*"), ""); static const QRegularExpression re_findRow("\\[(?<index>[A-Za-z0-9_]*)\\][\\s=]+(?<value>&?[A-Za-z0-9_]*)"); QRegularExpressionMatchIterator rowIter = re_findRow.globalMatch(arrayText); while (rowIter.hasNext()) { QRegularExpressionMatch match = rowIter.next(); QString key = match.captured("index"); QString value = match.captured("value"); map.insert(key, value); } return map; } int ParseUtil::gameStringToInt(QString gameString, bool * ok) { if (ok) *ok = true; if (QString::compare(gameString, "TRUE", Qt::CaseInsensitive) == 0) return 1; if (QString::compare(gameString, "FALSE", Qt::CaseInsensitive) == 0) return 0; return gameString.toInt(ok, 0); } bool ParseUtil::gameStringToBool(QString gameString, bool * ok) { return gameStringToInt(gameString, ok) != 0; } QMap<QString, QHash<QString, QString>> ParseUtil::readCStructs(const QString &filename, const QString &label, const QHash<int, QString> memberMap) { QString filePath = this->root + "/" + filename; auto cParser = fex::Parser(); auto tokens = fex::Lexer().LexFile(filePath.toStdString()); auto structs = cParser.ParseTopLevelObjects(tokens); QMap<QString, QHash<QString, QString>> structMaps; for (auto it = structs.begin(); it != structs.end(); it++) { QString structLabel = QString::fromStdString(it->first); if (structLabel.isEmpty()) continue; if (!label.isEmpty() && label != structLabel) continue; // Speed up parsing if only looking for a particular symbol QHash<QString, QString> values; int i = 0; for (const fex::ArrayValue &v : it->second.values()) { if (v.type() == fex::ArrayValue::Type::kValuePair) { QString key = QString::fromStdString(v.pair().first); QString value = QString::fromStdString(v.pair().second->ToString()); values.insert(key, value); } else { // For compatibility with structs that don't specify member names. if (memberMap.contains(i) && !values.contains(memberMap.value(i))) values.insert(memberMap.value(i), QString::fromStdString(v.ToString())); } i++; } structMaps.insert(structLabel, values); } return structMaps; } QList<QStringList> ParseUtil::getLabelMacros(const QList<QStringList> &list, const QString &label) { bool in_label = false; QList<QStringList> new_list; for (const auto ¶ms : list) { const QString macro = params.value(0); if (macro == ".label") { if (params.value(1) == label) { in_label = true; } else if (in_label) { // If nothing has been read yet, assume the label // we're looking for is in a stack of labels. if (new_list.length() > 0) { break; } } } else if (in_label) { new_list.append(params); } } return new_list; } // For if you don't care about filtering by macro, // and just want all values associated with some label. QStringList ParseUtil::getLabelValues(const QList<QStringList> &list, const QString &label) { const QList<QStringList> labelMacros = getLabelMacros(list, label); QStringList values; for (const auto ¶ms : labelMacros) { const QString macro = params.value(0); if (macro == ".align" || macro == ".ifdef" || macro == ".ifndef") { continue; } for (int i = 1; i < params.length(); i++) { values.append(params.value(i)); } } return values; } bool ParseUtil::tryParseJsonFile(QJsonDocument *out, const QString &filepath) { QFile file(filepath); if (!file.open(QIODevice::ReadOnly)) { logError(QString("Error: Could not open %1 for reading").arg(filepath)); return false; } const QByteArray data = file.readAll(); QJsonParseError parseError; const QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &parseError); file.close(); if (parseError.error != QJsonParseError::NoError) { logError(QString("Error: Failed to parse json file %1: %2").arg(filepath).arg(parseError.errorString())); return false; } *out = jsonDoc; return true; } bool ParseUtil::tryParseOrderedJsonFile(poryjson::Json::object *out, const QString &filepath) { QString err; QString jsonTxt = readTextFile(filepath); *out = OrderedJson::parse(jsonTxt, err).object_items(); if (!err.isEmpty()) { logError(QString("Error: Failed to parse json file %1: %2").arg(filepath).arg(err)); return false; } return true; } bool ParseUtil::ensureFieldsExist(const QJsonObject &obj, const QList<QString> &fields) { for (QString field : fields) { if (!obj.contains(field)) { logError(QString("JSON object is missing field '%1'.").arg(field)); return false; } } return true; } // QJsonValues are strictly typed, and so will not attempt any implicit conversions. // The below functions are for attempting to convert a JSON value read from the user's // project to a QString, int, or bool (whichever Porymap expects). QString ParseUtil::jsonToQString(QJsonValue value, bool * ok) { if (ok) *ok = true; switch (value.type()) { case QJsonValue::String: return value.toString(); case QJsonValue::Double: return QString::number(value.toInt()); case QJsonValue::Bool: return QString::number(value.toBool()); default: break; } if (ok) *ok = false; return QString(); } int ParseUtil::jsonToInt(QJsonValue value, bool * ok) { if (ok) *ok = true; switch (value.type()) { case QJsonValue::String: return ParseUtil::gameStringToInt(value.toString(), ok); case QJsonValue::Double: return value.toInt(); case QJsonValue::Bool: return value.toBool(); default: break; } if (ok) *ok = false; return 0; } bool ParseUtil::jsonToBool(QJsonValue value, bool * ok) { if (ok) *ok = true; switch (value.type()) { case QJsonValue::String: return ParseUtil::gameStringToBool(value.toString(), ok); case QJsonValue::Double: return value.toInt() != 0; case QJsonValue::Bool: return value.toBool(); default: break; } if (ok) *ok = false; return false; } int ParseUtil::getScriptLineNumber(const QString &filePath, const QString &scriptLabel) { if (scriptLabel.isEmpty()) return 0; if (filePath.endsWith(".inc") || filePath.endsWith(".s")) return getRawScriptLineNumber(readTextFile(filePath), scriptLabel); else if (filePath.endsWith(".pory")) return getPoryScriptLineNumber(readTextFile(filePath), scriptLabel); return 0; } int ParseUtil::getRawScriptLineNumber(QString text, const QString &scriptLabel) { text = removeStringLiterals(text); text = removeLineComments(text, "@"); QRegularExpressionMatchIterator it = re_incScriptLabel.globalMatch(text); while (it.hasNext()) { const QRegularExpressionMatch match = it.next(); if (match.captured("label") == scriptLabel) return text.left(match.capturedStart("label")).count('\n') + 1; } return 0; } int ParseUtil::getPoryScriptLineNumber(QString text, const QString &scriptLabel) { text = removeStringLiterals(text); text = removeLineComments(text, {"//", "#"}); QRegularExpressionMatchIterator it = re_poryScriptLabel.globalMatch(text); while (it.hasNext()) { const QRegularExpressionMatch match = it.next(); if (match.captured("label") == scriptLabel) return text.left(match.capturedStart("label")).count('\n') + 1; } QRegularExpressionMatchIterator raw_it = re_poryRawSection.globalMatch(text); while (raw_it.hasNext()) { const QRegularExpressionMatch match = raw_it.next(); const int relativelineNum = getRawScriptLineNumber(match.captured("raw_script"), scriptLabel); if (relativelineNum) return text.left(match.capturedStart("raw_script")).count('\n') + relativelineNum; } return 0; } QStringList ParseUtil::getGlobalScriptLabels(const QString &filePath) { if (filePath.endsWith(".inc") || filePath.endsWith(".s")) return getGlobalRawScriptLabels(readTextFile(filePath)); else if (filePath.endsWith(".pory")) return getGlobalPoryScriptLabels(readTextFile(filePath)); else return { }; } QStringList ParseUtil::getGlobalRawScriptLabels(QString text) { removeStringLiterals(text); removeLineComments(text, "@"); QStringList rawScriptLabels; QRegularExpressionMatchIterator it = re_globalIncScriptLabel.globalMatch(text); while (it.hasNext()) { const QRegularExpressionMatch match = it.next(); rawScriptLabels << match.captured("label"); } return rawScriptLabels; } QStringList ParseUtil::getGlobalPoryScriptLabels(QString text) { removeStringLiterals(text); removeLineComments(text, {"//", "#"}); QStringList poryScriptLabels; QRegularExpressionMatchIterator it = re_globalPoryScriptLabel.globalMatch(text); while (it.hasNext()) poryScriptLabels << it.next().captured("label"); QRegularExpressionMatchIterator raw_it = re_poryRawSection.globalMatch(text); while (raw_it.hasNext()) poryScriptLabels << getGlobalRawScriptLabels(raw_it.next().captured("raw_script")); return poryScriptLabels; } QString ParseUtil::removeStringLiterals(QString text) { static const QRegularExpression re_string("\".*\""); return text.remove(re_string); } QString ParseUtil::removeLineComments(QString text, const QString &commentSymbol) { const QRegularExpression re_lineComment(commentSymbol + "+.*"); return text.remove(re_lineComment); } QString ParseUtil::removeLineComments(QString text, const QStringList &commentSymbols) { for (const auto &commentSymbol : commentSymbols) text = removeLineComments(text, commentSymbol); return text; } #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) #include <QProcess> QStringList ParseUtil::splitShellCommand(QStringView command) { return QProcess::splitCommand(command); } #else // The source for QProcess::splitCommand() as of Qt 5.15.2 QStringList ParseUtil::splitShellCommand(QStringView command) { QStringList args; QString tmp; int quoteCount = 0; bool inQuote = false; // handle quoting. tokens can be surrounded by double quotes // "hello world". three consecutive double quotes represent // the quote character itself. for (int i = 0; i < command.size(); ++i) { if (command.at(i) == QLatin1Char('"')) { ++quoteCount; if (quoteCount == 3) { // third consecutive quote quoteCount = 0; tmp += command.at(i); } continue; } if (quoteCount) { if (quoteCount == 1) inQuote = !inQuote; quoteCount = 0; } if (!inQuote && command.at(i).isSpace()) { if (!tmp.isEmpty()) { args += tmp; tmp.clear(); } } else { tmp += command.at(i); } } if (!tmp.isEmpty()) args += tmp; return args; } #endif