diff options
author | Tomaz Canabrava <tcanabrava@kde.org> | 2020-01-18 19:06:07 +0000 |
---|---|---|
committer | Tomaz Canabrava <tcanabrava@kde.org> | 2020-01-22 14:17:56 +0000 |
commit | 95aee1294e32aca966dd306667386460cd12182d (patch) | |
tree | 8ef6afcf7f8ca76823527182b60ff48e5ee5b1fc /src/kconfig_compiler/KConfigXmlParser.cpp | |
parent | 68c1cfd0d8dcee761005a9eb08a57c224197fce2 (diff) | |
download | kconfig-95aee1294e32aca966dd306667386460cd12182d.tar.gz kconfig-95aee1294e32aca966dd306667386460cd12182d.tar.bz2 |
Refactor KConfigXT
Summary:
The current KConfigXT compiler is in a sad state:
It's a massive file with loads of global variables that handle state, the generator is done within the main() function and it seems to have grown organically. There are no classes to separate logic / state / generation, what exists is code that generates code from a xml / ini pair, but it's hard to even discover what a bit of code is doing. The code istyle is C++ / Java from the nineties, which is not bad per see but it also uses quite a few things that are going to be deprecated in Qt 6 so I'm also taking the time make the code more streamlined with newer code style (no iterators, lambdas, auto usage, etc).
The code that generates the files simplly pushes strings to a text stream, and it's hard to figure out when something starts or something ends: for instance, the code that generates the Constructor has more than sixty lines of code englobing some nested if - for - if - for constructs.
Currently the code is "done" - there's one bug that I still need to find & fix regarding Translations, but the rest seems sane.
The current testcode generates incorrect *whitespaces* regarding the old code (there's some parts that I feel that it's important to fix before merging, but overall, the whitespace changes are not bad and easier to handle, old code had a hand-counted amount of spaces before each line, new code has a function whitespace() that adds the current-and-correct amount of whitespaces based on indentation level that you start by startScope() and ends with endScope(). rest of the code still needs to be ported to it.
I plan to fix the testcases whitespace by manually adding them, I'v fougth with the code for a while and added a few hacks there but I don't want to make the code hackish again.
New code is not perfect by any means, but is a good step in the right direction.
This code tries to Separate the compiler code into many different files / classes to be more obvious what's happening, and each class also has many helper methods to minimize copypaste.
- CodeGenerator: Has base code for the header and source files that can be shared
- HeaderGenerator: Logic for generating the header file
- SourceGenerator: Logic for generating the source file
- KcfgParser: Logic for parsing the kcfg file and extracting the information from the Xml file
- CommonStructs: a header that contains the structs that are currently used everywhere.
- KConfigParameters: (was CfgConfig - ConfigConfig, wat) - Has information passed via the kcfgc file
- kcfg_compiler - will be renamed to main - start the other classes and generates the files.
This code here currently has the begining of this separation, with the CodeGenerator and the HeaderGenerator in a ~good~ state, but unfinished.
Test Plan:
- Run the test cases,
- Compare the diffs generated by the testcases and fix in the code the errors / differences
- Run and compare real kde source with the new and old generators to look for errors
Reviewers: #frameworks, ervin, bport, dfaure
Reviewed By: dfaure
Subscribers: davidre, bcooksley, cgiboudeaux, kossebau, bport, ngraham, kde-frameworks-devel
Tags: #frameworks
Differential Revision: https://phabricator.kde.org/D26202
Diffstat (limited to 'src/kconfig_compiler/KConfigXmlParser.cpp')
-rw-r--r-- | src/kconfig_compiler/KConfigXmlParser.cpp | 564 |
1 files changed, 564 insertions, 0 deletions
diff --git a/src/kconfig_compiler/KConfigXmlParser.cpp b/src/kconfig_compiler/KConfigXmlParser.cpp new file mode 100644 index 00000000..11c76abd --- /dev/null +++ b/src/kconfig_compiler/KConfigXmlParser.cpp @@ -0,0 +1,564 @@ +/* This file is part of the KDE libraries + Copyright (c) 2003 Cornelius Schumacher <schumacher@kde.org> + Copyright (c) 2003 Waldo Bastian <bastian@kde.org> + Copyright (c) 2003 Zack Rusin <zack@kde.org> + Copyright (c) 2006 Michaƫl Larouche <michael.larouche@kdemail.net> + Copyright (c) 2008 Allen Winter <winter@kde.org> + Copyright (C) 2020 Tomaz Cananbrava (tcanabrava@kde.org) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "KConfigXmlParser.h" +#include "KConfigParameters.h" + +#include <QDomAttr> +#include <QDomElement> +#include <QDomNode> +#include <QFile> +#include <QList> +#include <QStringList> +#include <QTextStream> + +namespace +{ +QTextStream cout(stdout); +QTextStream cerr(stderr); +} + +//TODO: Move preprocessDefault to Header / CPP implementation. +// it makes no sense for a parser to process those values and generate code. + +static void preProcessDefault(QString &defaultValue, const QString &name, + const QString &type, + const CfgEntry::Choices &choices, + QString &code, const KConfigParameters &cfg) +{ + if (type == QLatin1String("String") && !defaultValue.isEmpty()) { + defaultValue = literalString(defaultValue); + + } else if (type == QLatin1String("Path") && !defaultValue.isEmpty()) { + defaultValue = literalString(defaultValue); + } else if (type == QLatin1String("Url") && !defaultValue.isEmpty()) { + // Use fromUserInput in order to support absolute paths and absolute urls, like KDE4's KUrl(QString) did. + defaultValue = QLatin1String("QUrl::fromUserInput( ") + literalString(defaultValue) + QLatin1Char(')'); + } else if ((type == QLatin1String("UrlList") || type == QLatin1String("StringList") || type == QLatin1String("PathList")) && !defaultValue.isEmpty()) { + QTextStream cpp(&code, QIODevice::WriteOnly | QIODevice::Append); + if (!code.isEmpty()) { + cpp << endl; + } + + if (type == QLatin1String("UrlList")) { + cpp << " QList<QUrl> default" << name << ";" << endl; + } else { + cpp << " QStringList default" << name << ";" << endl; + } + const QStringList defaults = defaultValue.split(QLatin1Char(',')); + QStringList::ConstIterator it; + for (it = defaults.constBegin(); it != defaults.constEnd(); ++it) { + cpp << " default" << name << ".append( "; + if (type == QLatin1String("UrlList")) { + cpp << "QUrl::fromUserInput("; + } + cpp << "QString::fromUtf8( \"" << *it << "\" ) "; + if (type == QLatin1String("UrlList")) { + cpp << ") "; + } + cpp << ");" << endl; + } + defaultValue = QLatin1String("default") + name; + + } else if (type == QLatin1String("Color") && !defaultValue.isEmpty()) { + const QRegularExpression colorRe(QRegularExpression::anchoredPattern( + QStringLiteral("\\d+,\\s*\\d+,\\s*\\d+(,\\s*\\d+)?"))); + + if (colorRe.match(defaultValue).hasMatch()) { + defaultValue = QLatin1String("QColor( ") + defaultValue + QLatin1String(" )"); + } else { + defaultValue = QLatin1String("QColor( \"") + defaultValue + QLatin1String("\" )"); + } + + } else if (type == QLatin1String("Enum")) { + QList<CfgEntry::Choice>::ConstIterator it; + for (it = choices.choices.constBegin(); it != choices.choices.constEnd(); ++it) { + if ((*it).name == defaultValue) { + if (cfg.globalEnums && choices.name().isEmpty()) { + defaultValue.prepend(choices.prefix); + } else { + defaultValue.prepend(enumTypeQualifier(name, choices) + choices.prefix); + } + break; + } + } + + } else if (type == QLatin1String("IntList")) { + QTextStream cpp(&code, QIODevice::WriteOnly | QIODevice::Append); + if (!code.isEmpty()) { + cpp << endl; + } + + cpp << " QList<int> default" << name << ";" << endl; + if (!defaultValue.isEmpty()) { + const QStringList defaults = defaultValue.split(QLatin1Char(',')); + QStringList::ConstIterator it; + for (it = defaults.constBegin(); it != defaults.constEnd(); ++it) { + cpp << " default" << name << ".append( " << *it << " );" + << endl; + } + } + defaultValue = QLatin1String("default") + name; + } +} + +static QString dumpNode(const QDomNode &node) +{ + QString msg; + QTextStream s(&msg, QIODevice::WriteOnly); + node.save(s, 0); + + msg = msg.simplified(); + if (msg.length() > 40) { + return msg.left(37) + QLatin1String("..."); + } + return msg; +} + +void KConfigXmlParser::readParameterFromEntry(CfgEntry &readEntry, const QDomElement &e) +{ + readEntry.param = e.attribute(QStringLiteral("name")); + readEntry.paramType = e.attribute(QStringLiteral("type")); + + if (readEntry.param.isEmpty()) { + cerr << "Parameter must have a name: " << dumpNode(e) << endl; + exit (1); + } + + if (readEntry.paramType.isEmpty()) { + cerr << "Parameter must have a type: " << dumpNode(e) << endl; + exit(1); + } + + if ((readEntry.paramType == QLatin1String("Int")) || (readEntry.paramType == QLatin1String("UInt"))) { + bool ok; + readEntry.paramMax = e.attribute(QStringLiteral("max")).toInt(&ok); + if (!ok) { + cerr << "Integer parameter must have a maximum (e.g. max=\"0\"): " << dumpNode(e) << endl; + exit(1); + } + } else if (readEntry.paramType == QLatin1String("Enum")) { + for (QDomElement e2 = e.firstChildElement(); !e2.isNull(); e2 = e2.nextSiblingElement()) { + if (e2.tagName() == QLatin1String("values")) { + for (QDomElement e3 = e2.firstChildElement(); !e3.isNull(); e3 = e3.nextSiblingElement()) { + if (e3.tagName() == QLatin1String("value")) { + readEntry.paramValues.append(e3.text()); + } + } + break; + } + } + if (readEntry.paramValues.isEmpty()) { + cerr << "No values specified for parameter '" << readEntry.param << "'." << endl; + exit(1); + } + readEntry.paramMax = readEntry.paramValues.count() - 1; + } else { + cerr << "Parameter '" << readEntry.param << "' has type " << readEntry.paramType + << " but must be of type int, uint or Enum." << endl; + exit(1); + } +} + +bool KConfigXmlParser::hasDefaultCode(CfgEntry &readEntry, const QDomElement &element) +{ + for (QDomElement e = element.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) { + if (e.attribute(QStringLiteral("param")).isEmpty()) { + if (e.attribute(QStringLiteral("code")) == QLatin1String("true")) { + return true; + } + } + } + return false; +} + + +void KConfigXmlParser::readChoicesFromEntry(CfgEntry &readEntry, const QDomElement &e) +{ + QList<CfgEntry::Choice> chlist; + for (QDomElement e2 = e.firstChildElement(); !e2.isNull(); e2 = e2.nextSiblingElement()) { + if (e2.tagName() != QLatin1String("choice")) { + continue; + } + CfgEntry::Choice choice; + choice.name = e2.attribute(QStringLiteral("name")); + if (choice.name.isEmpty()) { + cerr << "Tag <choice> requires attribute 'name'." << endl; + } + for (QDomElement e3 = e2.firstChildElement(); !e3.isNull(); e3 = e3.nextSiblingElement()) { + if (e3.tagName() == QLatin1String("label")) { + choice.label = e3.text(); + choice.context = e3.attribute(QStringLiteral("context")); + } + if (e3.tagName() == QLatin1String("tooltip")) { + choice.toolTip = e3.text(); + choice.context = e3.attribute(QStringLiteral("context")); + } + if (e3.tagName() == QLatin1String("whatsthis")) { + choice.whatsThis = e3.text(); + choice.context = e3.attribute(QStringLiteral("context")); + } + } + chlist.append(choice); + } + + QString name = e.attribute(QStringLiteral("name")); + QString prefix = e.attribute(QStringLiteral("prefix")); + + readEntry.choices = CfgEntry::Choices(chlist, name, prefix); +} + +void KConfigXmlParser::readGroupElements(CfgEntry &readEntry, const QDomElement &element) +{ + for (QDomElement e = element.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) { + QString tag = e.tagName(); + if (tag == QLatin1String("label")) { + readEntry.label = e.text(); + readEntry.labelContext = e.attribute(QStringLiteral("context")); + } else if (tag == QLatin1String("tooltip")) { + readEntry.toolTip = e.text(); + readEntry.toolTipContext = e.attribute(QStringLiteral("context")); + } else if (tag == QLatin1String("whatsthis")) { + readEntry.whatsThis = e.text(); + readEntry.whatsThisContext = e.attribute(QStringLiteral("context")); + } else if (tag == QLatin1String("min")) { + readEntry.min = e.text(); + } else if (tag == QLatin1String("max")) { + readEntry.max = e.text(); + } else if (tag == QLatin1String("code")) { + readEntry.code = e.text(); + } else if (tag == QLatin1String("parameter")) { + readParameterFromEntry(readEntry, e); + } else if (tag == QLatin1String("default")) { + if (e.attribute(QStringLiteral("param")).isEmpty()) { + readEntry.defaultValue = e.text(); + } + } else if (tag == QLatin1String("choices")) { + readChoicesFromEntry(readEntry, e); + } else if (tag == QLatin1String("emit")) { + Signal signal; + signal.name = e.attribute(QStringLiteral("signal")); + readEntry.signalList.append(signal); + } + } +} + +void KConfigXmlParser::createChangedSignal(CfgEntry &readEntry) +{ + if (cfg.generateProperties && (cfg.allMutators || cfg.mutators.contains(readEntry.name))) { + Signal s; + s.name = changeSignalName(readEntry.name); + s.modify = true; + readEntry.signalList.append(s); + } +} + +void KConfigXmlParser::validateNameAndKey(CfgEntry &readEntry, const QDomElement &element) +{ + bool nameIsEmpty = readEntry.name.isEmpty(); + if (nameIsEmpty && readEntry.key.isEmpty()) { + cerr << "Entry must have a name or a key: " << dumpNode(element) << endl; + exit (1); + } + + if (readEntry.key.isEmpty()) { + readEntry.key = readEntry.name; + } + + if (nameIsEmpty) { + readEntry.name = readEntry.key; + readEntry.name.remove(QLatin1Char(' ')); + } else if (readEntry.name.contains(QLatin1Char(' '))) { + cout << "Entry '" << readEntry.name << "' contains spaces! <name> elements can not contain spaces!" << endl; + readEntry.name.remove(QLatin1Char(' ')); + } + + if (readEntry.name.contains(QStringLiteral("$("))) { + if (readEntry.param.isEmpty()) { + cerr << "Name may not be parameterized: " << readEntry.name << endl; + exit (1); + } + } else { + if (!readEntry.param.isEmpty()) { + cerr << "Name must contain '$(" << readEntry.param << ")': " << readEntry.name << endl; + exit (1); + } + } +} + +void KConfigXmlParser::readParamDefaultValues(CfgEntry &readEntry, const QDomElement &element) +{ + if (readEntry.param.isEmpty()) { + return; + } + // Adjust name + readEntry.paramName = readEntry.name; + + readEntry.name.remove(QStringLiteral("$(") + readEntry.param + QLatin1Char(')')); + // Lookup defaults for indexed entries + for (int i = 0; i <= readEntry.paramMax; i++) { + readEntry.paramDefaultValues.append(QString()); + } + + for (QDomElement e = element.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) { + QString tag = e.tagName(); + if (tag != QLatin1String("default")) { + continue; + } + QString index = e.attribute(QStringLiteral("param")); + if (index.isEmpty()) { + continue; + } + + bool ok; + int i = index.toInt(&ok); + if (!ok) { + i = readEntry.paramValues.indexOf(index); + if (i == -1) { + cerr << "Index '" << index << "' for default value is unknown." << endl; + exit (1); + } + } + + if ((i < 0) || (i > readEntry.paramMax)) { + cerr << "Index '" << i << "' for default value is out of range [0, " << readEntry.paramMax << "]." << endl; + exit (1); + } + + QString tmpDefaultValue = e.text(); + + if (e.attribute(QStringLiteral("code")) != QLatin1String("true")) { + preProcessDefault(tmpDefaultValue, readEntry.name, readEntry.type, readEntry.choices, readEntry.code, cfg); + } + + readEntry.paramDefaultValues[i] = tmpDefaultValue; + } +} + +CfgEntry *KConfigXmlParser::parseEntry(const QString &group, const QDomElement &element) +{ + CfgEntry readEntry; + readEntry.type = element.attribute(QStringLiteral("type")); + readEntry.name = element.attribute(QStringLiteral("name")); + readEntry.key = element.attribute(QStringLiteral("key")); + readEntry.hidden = element.attribute(QStringLiteral("hidden")) == QLatin1String("true");; + readEntry.group = group; + + const bool nameIsEmpty = readEntry.name.isEmpty(); + + readGroupElements(readEntry, element); + + createChangedSignal(readEntry); + validateNameAndKey(readEntry, element); + + if (readEntry.label.isEmpty()) { + readEntry.label = readEntry.key; + } + + if (readEntry.type.isEmpty()) { + readEntry.type = QStringLiteral("String"); // XXX : implicit type might be bad + } + + readParamDefaultValues(readEntry, element); + + if (!mValidNameRegexp.match(readEntry.name).hasMatch()) { + if (nameIsEmpty) + cerr << "The key '" << readEntry.key << "' can not be used as name for the entry because " + "it is not a valid name. You need to specify a valid name for this entry." << endl; + else { + cerr << "The name '" << readEntry.name << "' is not a valid name for an entry." << endl; + } + exit (1); + } + + if (mAllNames.contains(readEntry.name)) { + if (nameIsEmpty) + cerr << "The key '" << readEntry.key << "' can not be used as name for the entry because " + "it does not result in a unique name. You need to specify a unique name for this entry." << endl; + else { + cerr << "The name '" << readEntry.name << "' is not unique." << endl; + } + exit (1); + } + + mAllNames.append(readEntry.name); + + if (!hasDefaultCode(readEntry, element)) { + // TODO: Move all the options to CfgEntry. + preProcessDefault(readEntry.defaultValue, readEntry.name, readEntry.type, readEntry.choices, readEntry.code, cfg); + } + + // TODO: Try to Just return the CfgEntry we populated instead of + // creating another one to fill the code. + CfgEntry *result = new CfgEntry(); + result->group = readEntry.group; + result->type = readEntry.type; + result->key = readEntry.key; + result->name = readEntry.name; + result->labelContext = readEntry.labelContext; + result->label = readEntry.label; + result->toolTipContext = readEntry.toolTipContext; + result->toolTip = readEntry.toolTip; + result->whatsThisContext = readEntry.whatsThisContext; + result->whatsThis = readEntry.whatsThis; + result->code = readEntry.code; + result->defaultValue = readEntry.defaultValue; + result->choices = readEntry.choices; + result->signalList = readEntry.signalList; + result->hidden = readEntry.hidden; + + if (!readEntry.param.isEmpty()) { + result->param = readEntry.param; + result->paramName = readEntry.paramName; + result->paramType = readEntry.paramType; + result->paramValues = readEntry.paramValues; + result->paramDefaultValues = readEntry.paramDefaultValues; + result->paramMax = readEntry.paramMax; + } + result->min = readEntry.min; + result->max = readEntry.max; + + return result; +} + +// TODO: Change the name of the config variable. +KConfigXmlParser::KConfigXmlParser(const KConfigParameters &cfg, const QString &inputFileName) + : cfg(cfg), mInputFileName(inputFileName) +{ + mValidNameRegexp.setPattern(QRegularExpression::anchoredPattern(QStringLiteral("[a-zA-Z_][a-zA-Z0-9_]*"))); +} + +void KConfigXmlParser::start() +{ + QFile input(mInputFileName); + QDomDocument doc; + QString errorMsg; + int errorRow; + int errorCol; + if (!doc.setContent(&input, &errorMsg, &errorRow, &errorCol)) { + cerr << "Unable to load document." << endl; + cerr << "Parse error in " << mInputFileName << ", line " << errorRow << ", col " << errorCol << ": " << errorMsg << endl; + exit (1); + } + + QDomElement cfgElement = doc.documentElement(); + if (cfgElement.isNull()) { + cerr << "No document in kcfg file" << endl; + exit (1); + } + + for (QDomElement element = cfgElement.firstChildElement(); !element.isNull(); element = element.nextSiblingElement()) { + QString tag = element.tagName(); + + if (tag == QLatin1String("include")) { + readIncludeTag(element); + } else if (tag == QLatin1String("kcfgfile")) { + readKcfgfileTag(element); + } else if (tag == QLatin1String("group")) { + readGroupTag(element); + } else if (tag == QLatin1String("signal")) { + readSignalTag(element); + } + } +} + +ParseResult KConfigXmlParser::getParseResult() const +{ + return mParseResult; +} + +void KConfigXmlParser::readIncludeTag(const QDomElement &e) +{ + QString includeFile = e.text(); + if (!includeFile.isEmpty()) { + mParseResult.includes.append(includeFile); + } +} + +void KConfigXmlParser::readGroupTag(const QDomElement &e) +{ + QString group = e.attribute(QStringLiteral("name")); + if (group.isEmpty()) { + cerr << "Group without name" << endl; + exit (1); + } + + for (QDomElement e2 = e.firstChildElement(); !e2.isNull(); e2 = e2.nextSiblingElement()) { + if (e2.tagName() != QLatin1String("entry")) { + continue; + } + CfgEntry *entry = parseEntry(group, e2); + if (entry) { + mParseResult.entries.append(entry); + } else { + cerr << "Can not parse entry." << endl; + exit (1); + } + } +} + +void KConfigXmlParser::readKcfgfileTag(const QDomElement &e) +{ + mParseResult.cfgFileName = e.attribute(QStringLiteral("name")); + mParseResult.cfgFileNameArg = e.attribute(QStringLiteral("arg")).toLower() == QLatin1String("true"); + for (QDomElement e2 = e.firstChildElement(); !e2.isNull(); e2 = e2.nextSiblingElement()) { + if (e2.tagName() == QLatin1String("parameter")) { + Param p; + p.name = e2.attribute(QStringLiteral("name")); + p.type = e2.attribute(QStringLiteral("type")); + if (p.type.isEmpty()) { + p.type = QStringLiteral("String"); + } + mParseResult.parameters.append(p); + } + } +} + +void KConfigXmlParser::readSignalTag(const QDomElement &e) +{ + QString signalName = e.attribute(QStringLiteral("name")); + if (signalName.isEmpty()) { + cerr << "Signal without name." << endl; + exit (1); + } + Signal theSignal; + theSignal.name = signalName; + + for (QDomElement e2 = e.firstChildElement(); !e2.isNull(); e2 = e2.nextSiblingElement()) { + if (e2.tagName() == QLatin1String("argument")) { + Param argument; + argument.type = e2.attribute(QStringLiteral("type")); + if (argument.type.isEmpty()) { + cerr << "Signal argument without type." << endl; + exit (1); + } + argument.name= e2.text(); + theSignal.arguments.append(argument); + } else if (e2.tagName() == QLatin1String("label")) { + theSignal.label = e2.text(); + } + } + + mParseResult.signalList.append(theSignal); +} |