diff --git a/src/indexer/katehighlightingindexer.cpp b/src/indexer/katehighlightingindexer.cpp --- a/src/indexer/katehighlightingindexer.cpp +++ b/src/indexer/katehighlightingindexer.cpp @@ -32,6 +32,10 @@ #include #endif +#include +#include +#include + namespace { QStringList readListing(const QString &fileName) @@ -117,13 +121,15 @@ const QString c = xml.attributes().value(QLatin1String("char")).toString(); if (c.size() != 1) { qWarning() << hlFilename << "line" << xml.lineNumber() << "'char' must contain exactly one char:" << c; + return false; } } if (testChar1) { const QString c = xml.attributes().value(QLatin1String("char1")).toString(); if (c.size() != 1) { qWarning() << hlFilename << "line" << xml.lineNumber() << "'char1' must contain exactly one char:" << c; + return false; } } @@ -375,6 +381,182 @@ QSet m_existingAttributeNames; }; +//! Proposes to replace StringDetect with DetectChar or Detect2Chars +bool suggestForStringDetect(const QString &hlFilename, QXmlStreamReader &xml) +{ + if (xml.name() == QLatin1String("StringDetect")) { + const auto insensitive = xml.attributes().value(QLatin1String("insensitive")); + if (insensitive != QStringLiteral("true")) { + const auto str = xml.attributes().value(QLatin1String("String")).toString(); + if (str.size() == 1) { + qWarning() << hlFilename << "line" << xml.lineNumber() << "StringDetect candidate for DetectChar:" << str; + return false; + } + else if (str.size() == 2) { + qWarning() << hlFilename << "line" << xml.lineNumber() << "StringDetect candidate for Detect2Chars:" << str; + return false; + } + } + } + + return true; +} + +//! Proposes to replace AnyChar with DetectChar +bool suggestForAnyChar(const QString &hlFilename, QXmlStreamReader &xml) +{ + if (xml.name() == QLatin1String("AnyChar")) { + const auto string = xml.attributes().value(QLatin1String("String")).toString(); + if (string.size() == 1) { + qWarning() << hlFilename << "line" << xml.lineNumber() << "AnyChar candidate for DetectChar:" << string; + return false; + } + } + + return true; +} + +struct SuggestPattern +{ + QLatin1String suggest; + QRegularExpression regex; + bool hasInsensitiveAttribute; + + SuggestPattern(const char *suggest, const QString &stringRegex, bool hasInsensitiveAttribute) + : suggest(suggest) + , regex(stringRegex) + , hasInsensitiveAttribute(hasInsensitiveAttribute) + { + Q_ASSERT(regex.isValid()); + } +}; + +#define CharReg R"((?:\\[^a-su-zA-Z]|[^^$.+*?|{\[\\]))" +SuggestPattern suggestPatterns[]{ + {"HlCHex", QStringLiteral(R"(^0x\[(?:0-9|a-f|A-F){3}\][*+]$)"), false}, + {"HlCStringChar", QStringLiteral(R"(^\\\[[abefnrtv"'?]{11}\]$)"), false}, + {"RangeDetect", QStringLiteral("^(" CharReg R"()\[\^\1\]\*\1$)"), false}, + {"Int", QStringLiteral(R"(^(?:\[0-9\]|\\d)(?:\+|(?:(?:\[0-9\]|\\d)\*))$)"), false}, + {"LineContinue", QStringLiteral("^" CharReg "\\$$"), false}, + {"LineContinue with column=\"0\"", QStringLiteral("^\\^" CharReg "\\$$"), false}, + {"DetectChar", QStringLiteral("^" CharReg "$"), false}, + {"Detect2Chars", QStringLiteral("^" CharReg CharReg "$"), false}, + {"DetectChar with column=\"0\"", QStringLiteral("^\\^" CharReg "$"), false}, + {"Detect2Chars with column=\"0\"", QStringLiteral("^\\^" CharReg CharReg "$"), false}, + {"AnyChar", QStringLiteral(R"(^\^?\[(?:\\\]|[^\]])+\]\$?$)"), true}, + {"StringDetect", QStringLiteral("^" CharReg "*$"), true}, + {"WordDetect", QStringLiteral(R"(^\\b)" CharReg R"(*\\b$)"), true}, + {"DetectIdentifier", QStringLiteral(R"(^\[(?:_|a-z){2}\]\[(?:(?:0-9|a-z|_){3}|\\w)\]\*$)"), false}, + {"DetectIdentifier", QStringLiteral(R"(^\[(?:_|a-z|A-Z){3}\]\[(?:(?:0-9|a-z|A-Z|_){4}|\\w)\]\*$)"), true}, +}; +#undef CharReg + +//! Proposes to replace RegExpr with another element (HlCHex, HlCStringChar, RangeDetect, etc) +bool suggestForRegExp(const QString &hlFilename, QXmlStreamReader &xml) +{ + if (xml.name() == QLatin1String("RegExpr")) { + auto string = xml.attributes().value(QLatin1String("String")).toString(); + if (xml.attributes().hasAttribute(QLatin1String("lookAhead"))) { + const auto lookAhead = xml.attributes().value(QLatin1String("lookAhead")); + if (string == QLatin1String("^$") || (lookAhead == QLatin1String("true") && string == QLatin1String("."))) { + const auto context = xml.attributes().value(QLatin1String("context")); + qWarning() << hlFilename << "line" << xml.lineNumber() + << QStringLiteral("RegExpr candidate for fallthroughContext=\"") + context + QStringLiteral("\" fallthrough=\"true\""); + return false; + } + } + + const auto insensitive = xml.attributes().value(QLatin1String("insensitive")); + const bool isFirstNonSpace = string.startsWith(QLatin1String("^\\s*")); + if (isFirstNonSpace) { + string.remove(0, 4); + } + for (const auto &suggestPattern : suggestPatterns) { + if (insensitive == QLatin1String("true") && !suggestPattern.hasInsensitiveAttribute) { + continue; + } + if (suggestPattern.regex.match(string).hasMatch()) { + const auto extra = (isFirstNonSpace ? " with firstNonSpace=\"true\":" : ":"); + qWarning() << hlFilename << "line" << xml.lineNumber() << "RegExpr candidate for " << suggestPattern.suggest << extra << string; + return false; + } + } + } + + return true; +} + +/** + * Helper class that suggests rule mergers + */ +class RuleMergeSuggest +{ +public: + bool suggest(const QString &hlFilename, QXmlStreamReader &xml) + { + if (xml.attributes().hasAttribute(QLatin1String("attribute")) && xml.name() != QLatin1String("context")) { + const std::size_t specificAttributeLength = 3; + const QStringRef values[] { + xml.attributes().value(QLatin1String("String")), + xml.attributes().value(QLatin1String("char")), + xml.attributes().value(QLatin1String("char1")), + xml.name(), + xml.attributes().value(QLatin1String("attribute")), + xml.attributes().value(QLatin1String("context")), + xml.attributes().value(QLatin1String("beginRegion")), + xml.attributes().value(QLatin1String("endRegion")), + xml.attributes().value(QLatin1String("lookAhead")), + xml.attributes().value(QLatin1String("firstNonSpace")), + xml.attributes().value(QLatin1String("column")), + }; + // check the size of the tables and display the number of elements in a compilation error in case of mismatch + std::extent::type{} = std::extent::type{}; + + if (std::equal(std::begin(values) + specificAttributeLength, std::end(values), std::begin(m_previousRule) + specificAttributeLength) + && (canBeMerged(m_previousRule, values) + || canBeMerged(values, m_previousRule) + || std::equal(std::begin(values), std::end(values) - specificAttributeLength, std::begin(m_previousRule)) + )) { + qWarning() << hlFilename << "line" << xml.lineNumber() << xml.name() << "can be merged with the previous rule"; + return false; + } + + std::size_t i = 0; + for (auto& stringRef : values) { + m_previousRule[i] = stringRef.toString(); + ++i; + } + } + else { + m_previousRule[3].clear(); + } + + return true; + } + +private: + template + bool canBeMerged(const Array1 &array1, const Array2 &array2) { + if (array1[3] == QLatin1String("RegExpr")) { + return true; + } + if (array1[3] == QLatin1String("DetectChar")) { + if (array2[3] == QLatin1String("AnyChar")) { + return true; + } + if (array2[3] == QLatin1String("RangeDetect")) { + const auto cleft = array2[1][0].digitValue(); + const auto cright = array2[2][0].digitValue(); + const auto c = array1[1][0].digitValue(); + return ((cleft <= c && c <= cright) || (c + 1 == cleft) || (cright + 1 == c)); + } + } + return false; + } + + QString m_previousRule[11]; +}; + } int main(int argc, char *argv[]) @@ -464,6 +646,7 @@ AttributeChecker attributeChecker(hlFilename); KeywordChecker keywordChecker(hlFilename); + RuleMergeSuggest ruleMergeSuggest; const QString hlName = hl[QStringLiteral("name")].toString(); // scan for broken regex or keywords with spaces @@ -505,6 +688,30 @@ anyError = 7; continue; } + + // suggestions for StringDetect + if (!suggestForStringDetect(hlFilename, xml)) { + //anyError = 12; + continue; + } + + // suggestions for AnyChar + if (!suggestForAnyChar(hlFilename, xml)) { + //anyError = 12; + continue; + } + + // suggestions for RegExp + if (!suggestForRegExp(hlFilename, xml)) { + //anyError = 12; + continue; + } + + // suggestions for a rule merge + if (ruleMergeSuggest.suggest(hlFilename, xml)) { + //anyError = 12; + continue; + } } if (!attributeChecker.check()) {