WingHexExplorer2/src/class/aspreprocesser.cpp

500 lines
18 KiB
C++

/*==============================================================================
** Copyright (C) 2024-2027 WingSummer
**
** This program is free software: you can redistribute it and/or modify it under
** the terms of the GNU Affero General Public License as published by the Free
** Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT
** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
** details.
**
** You should have received a copy of the GNU Affero General Public License
** along with this program. If not, see <https://www.gnu.org/licenses/>.
** =============================================================================
*/
#include "aspreprocesser.h"
#include <QDir>
#include <QFileInfo>
Q_GLOBAL_STATIC_WITH_ARGS(
QStringList, DEFAULT_MARCO,
({"__AS_ARRAY__", "__AS_ANY__", "__AS_GRID__", "__AS_HANDLE__",
"__AS_MATH__", "__AS_WEAKREF__", "__AS_COROUTINE__", "__WING_FILE__",
"__WING_STRING__", "__WING_COLOR__", "__WING_JSON__", "__WING_REGEX__",
"__WING_DICTIONARY__"}));
AsPreprocesser::AsPreprocesser(asIScriptEngine *engine) : engine(engine) {
Q_ASSERT(engine);
includeCallback = nullptr;
includeParam = nullptr;
pragmaCallback = nullptr;
pragmaParam = nullptr;
definedWords = *DEFAULT_MARCO;
}
AsPreprocesser::~AsPreprocesser() { void ClearAll(); }
int AsPreprocesser::loadSectionFromFile(const QString &filename) {
// The file name stored in the set should be the fully resolved name because
// it is possible to name the same file in multiple ways using relative
// paths.
auto fullpath = QFileInfo(filename).absoluteFilePath();
if (includeIfNotAlreadyIncluded(fullpath)) {
int r = loadScriptSection(fullpath);
if (r < 0)
return r;
else
return 1;
}
return 0;
}
int AsPreprocesser::loadSectionFromMemory(const QString &section,
const QByteArray &code) {
int r = processScriptSection(code, section);
if (r < 0)
return r;
else
return 1;
}
QList<AsPreprocesser::ScriptData> AsPreprocesser::scriptData() const {
return modifiedScripts;
}
asIScriptEngine *AsPreprocesser::getEngine() { return engine; }
void AsPreprocesser::setIncludeCallback(INCLUDECALLBACK_t callback,
void *userParam) {
includeCallback = callback;
includeParam = userParam;
}
void AsPreprocesser::setPragmaCallback(PRAGMACALLBACK_t callback,
void *userParam) {
pragmaCallback = callback;
pragmaParam = userParam;
}
void AsPreprocesser::defineWord(const QString &word) {
if (!definedWords.contains(word)) {
definedWords.append(word);
}
}
unsigned int AsPreprocesser::sectionCount() const {
return (unsigned int)(includedScripts.size());
}
QString AsPreprocesser::sectionName(unsigned int idx) const {
if (qsizetype(idx) >= qsizetype(includedScripts.size()))
return {};
return includedScripts.at(idx);
}
void AsPreprocesser::clearAll() { includedScripts.clear(); }
int AsPreprocesser::processScriptSection(const QByteArray &script,
const QString &sectionname) {
QVector<QPair<QString, bool>> includes;
QByteArray modifiedScript = script;
// First perform the checks for #if directives to exclude code that
// shouldn't be compiled
QByteArray::size_type pos = 0;
int nested = 0;
while (pos < modifiedScript.size()) {
asUINT len = 0;
asETokenClass t = engine->ParseToken(modifiedScript.data() + pos,
modifiedScript.size() - pos, &len);
if (t == asTC_UNKNOWN && modifiedScript[pos] == '#' &&
(pos + 1 < modifiedScript.size())) {
int start = pos++;
// Is this an #if directive?
t = engine->ParseToken(modifiedScript.data() + pos,
modifiedScript.size() - pos, &len);
Q_UNUSED(t);
QByteArray token = modifiedScript.mid(pos, len);
pos += len;
if (token == "if") {
t = engine->ParseToken(modifiedScript.data() + pos,
modifiedScript.size() - pos, &len);
if (t == asTC_WHITESPACE) {
pos += len;
t = engine->ParseToken(modifiedScript.data() + pos,
modifiedScript.size() - pos, &len);
}
if (t == asTC_IDENTIFIER) {
QByteArray word = modifiedScript.mid(pos, len);
// Overwrite the #if directive with space characters to
// avoid compiler error
pos += len;
overwriteCode(modifiedScript, start, pos - start);
// Has this identifier been defined by the application or
// not?
if (!definedWords.contains(word)) {
// Exclude all the code until and including the #endif
pos = excludeCode(modifiedScript, pos);
} else {
nested++;
}
}
} else if (token == "endif") {
// Only remove the #endif if there was a matching #if
if (nested > 0) {
overwriteCode(modifiedScript, start, pos - start);
nested--;
}
}
} else
pos += len;
}
// Then check for pre-processor directives
pos = 0;
while (pos >= 0 && pos < modifiedScript.size()) {
asUINT len = 0;
asETokenClass t = engine->ParseToken(modifiedScript.data() + pos,
modifiedScript.size() - pos, &len);
if (t == asTC_COMMENT || t == asTC_WHITESPACE) {
pos += len;
continue;
}
QString token = modifiedScript.mid(pos, len);
// Skip possible decorators before class and interface declarations
if (token == "shared" || token == "abstract" || token == "mixin" ||
token == "external") {
pos += len;
continue;
}
// Is this a preprocessor directive?
if (token == "#" && (pos + 1 < modifiedScript.size())) {
int start = pos++;
t = engine->ParseToken(modifiedScript.data() + pos,
modifiedScript.size() - pos, &len);
if (t == asTC_IDENTIFIER) {
token = modifiedScript.mid(pos, len);
if (token == "include") {
pos += len;
t = engine->ParseToken(modifiedScript.data() + pos,
modifiedScript.size() - pos, &len);
if (t == asTC_WHITESPACE) {
pos += len;
t = engine->ParseToken(modifiedScript.data() + pos,
modifiedScript.size() - pos,
&len);
}
if (t == asTC_VALUE && len > 2 &&
(modifiedScript[pos] == '"' ||
modifiedScript[pos] == '\'')) {
// Get the include file
QString includefile =
modifiedScript.mid(pos + 1, len - 2);
pos += len;
// Make sure the includeFile doesn't contain any
// line breaks
auto p = includefile.indexOf('\n');
if (p >= 0) {
// TODO: Show the correct line number for the
// error
auto str =
QObject::tr("Invalid file name for #include; "
"it contains a line-break: ") +
QStringLiteral("'") + includefile.left(p) +
QStringLiteral("'");
engine->WriteMessage(sectionname.toUtf8(), 0, 0,
asMSGTYPE_ERROR, str.toUtf8());
} else {
// Store it for later processing
includes.append({includefile, true});
// Overwrite the include directive with space
// characters to avoid compiler error
overwriteCode(modifiedScript, start, pos - start);
}
}
if (t == asTC_KEYWORD && modifiedScript[pos] == '<') {
pos += len;
// find the next '>'
auto rpos = pos;
bool found = false;
for (; rpos < modifiedScript.size(); ++rpos) {
if (modifiedScript[rpos] == '>') {
found = true;
break;
}
if (modifiedScript[rpos] == '\n') {
break;
}
}
if (found) {
QString includefile =
modifiedScript.mid(pos, rpos - pos).trimmed();
pos = rpos + 1;
// Make sure the includeFile doesn't contain any
// line breaks
auto p = includefile.indexOf('\n');
auto ws = includefile.indexOf(' ');
if (!includefile.isEmpty() && p >= 0 && ws >= 0) {
// TODO: Show the correct line number for
// the error
auto str =
QObject::tr(
"Invalid file name for #include; "
"it contains a line-break: ") +
QStringLiteral("'") + includefile.left(p) +
QStringLiteral("'");
engine->WriteMessage(sectionname.toUtf8(), 0, 0,
asMSGTYPE_ERROR,
str.toUtf8());
} else {
// Store it for later processing
includes.append({includefile, false});
// Overwrite the include directive with
// space characters to avoid compiler error
overwriteCode(modifiedScript, start,
pos - start);
}
} else {
auto str =
QObject::tr("Invalid file name for #include; "
"it contains a line-break or "
"unpaired symbol");
engine->WriteMessage(sectionname.toUtf8(), 0, 0,
asMSGTYPE_ERROR, str.toUtf8());
}
}
} else if (token == "pragma") {
// Read until the end of the line
pos += len;
for (; pos < modifiedScript.size() &&
modifiedScript[pos] != '\n';
pos++)
;
// Call the pragma callback
auto pragmaText =
modifiedScript.mid(start + 7, pos - start - 7);
// Overwrite the pragma directive with space characters
// to avoid compiler error
overwriteCode(modifiedScript, start, pos - start);
int r = pragmaCallback
? pragmaCallback(pragmaText, this, sectionname,
pragmaParam)
: -1;
if (r < 0) {
// TODO: Report the correct line number
engine->WriteMessage(
sectionname.toUtf8(), 0, 0, asMSGTYPE_ERROR,
QObject::tr("Invalid #pragma directive").toUtf8());
return r;
}
}
}
}
// Don't search for includes within statement blocks or
// between tokens in statements
else {
pos = skipStatement(modifiedScript, pos);
}
}
// Build the actual script
engine->SetEngineProperty(asEP_COPY_SCRIPT_SECTIONS, true);
addScriptSection(sectionname, modifiedScript);
if (includes.size() > 0) {
// If the callback has been set, then call it for each included file
if (includeCallback) {
for (QVector<QString>::size_type n = 0; n < includes.size(); n++) {
auto inc = includes[n];
int r = includeCallback(inc.first, inc.second, sectionname,
this, includeParam);
if (r < 0)
return r;
}
} else {
// By default we try to load the included file from the relative
// directory of the current file
// Determine the path of the current script so that we can resolve
// relative paths for includes
auto path = QFileInfo(sectionname).filePath();
// Load the included scripts
for (QVector<QString>::size_type n = 0; n < includes.size(); n++) {
// If the include is a relative path, then prepend the path of
// the originating script
auto inc = includes.at(n);
if (!QFileInfo(inc.first).isAbsolute()) {
includes[n].first = path + QDir::separator() + inc.first;
}
// Include the script section
int r = loadSectionFromFile(includes[n].first);
if (r < 0)
return r;
}
}
}
return 0;
}
int AsPreprocesser::loadScriptSection(const QString &filename) {
// Open the script file
QFile f(filename);
if (!f.open(QFile::ReadOnly)) {
// Write a message to the engine's message callback
auto msg = QObject::tr("Failed to open script file ") +
QStringLiteral("'") +
QFileInfo(filename).absoluteFilePath() + QStringLiteral("'");
engine->WriteMessage(filename.toUtf8(), 0, 0, asMSGTYPE_ERROR,
msg.toUtf8());
// TODO: Write the file where this one was included from
return -1;
}
// Read the entire file
auto code = f.readAll();
f.close();
// Process the script section even if it is zero length so that the name is
// registered
return processScriptSection(code, filename);
}
bool AsPreprocesser::includeIfNotAlreadyIncluded(const QString &filename) {
if (includedScripts.contains(filename)) {
// Already included
return false;
}
// Add the file to the set of included sections
includedScripts.append(filename);
return true;
}
int AsPreprocesser::skipStatement(const QByteArray &modifiedScript, int pos) {
asUINT len = 0;
// Skip until ; or { whichever comes first
while (pos < (int)modifiedScript.length() && modifiedScript[pos] != ';' &&
modifiedScript[pos] != '{') {
engine->ParseToken(modifiedScript.data() + pos,
modifiedScript.size() - pos, &len);
pos += len;
}
// Skip entire statement block
if (pos < (int)modifiedScript.length() && modifiedScript[pos] == '{') {
pos += 1;
// Find the end of the statement block
int level = 1;
while (level > 0 && pos < (int)modifiedScript.size()) {
asETokenClass t = engine->ParseToken(
modifiedScript.data() + pos, modifiedScript.size() - pos, &len);
if (t == asTC_KEYWORD) {
if (modifiedScript[pos] == '{')
level++;
else if (modifiedScript[pos] == '}')
level--;
}
pos += len;
}
} else
pos += 1;
return pos;
}
int AsPreprocesser::excludeCode(QByteArray &modifiedScript, int pos) {
asUINT len = 0;
int nested = 0;
while (pos < (int)modifiedScript.size()) {
engine->ParseToken(modifiedScript.data() + pos,
modifiedScript.size() - pos, &len);
if (modifiedScript[pos] == '#') {
modifiedScript[pos] = ' ';
pos++;
// Is it an #if or #endif directive?
engine->ParseToken(modifiedScript.data() + pos,
modifiedScript.size() - pos, &len);
QString token = modifiedScript.mid(pos, len);
overwriteCode(modifiedScript, pos, len);
if (token == "if") {
nested++;
} else if (token == "endif") {
if (nested-- == 0) {
pos += len;
break;
}
}
} else if (modifiedScript[pos] != '\n') {
overwriteCode(modifiedScript, pos, len);
}
pos += len;
}
return pos;
}
void AsPreprocesser::overwriteCode(QByteArray &modifiedScript, int start,
int len) {
auto code = modifiedScript.data() + start;
for (int n = 0; n < len; n++) {
if (*code != '\n')
*code = ' ';
code++;
}
}
void AsPreprocesser::addScriptSection(const QString &section,
const QByteArray &code) {
ScriptData data;
data.section = section;
data.script = code;
modifiedScripts.append(data);
}