10#include <QCoreApplication>
14#include <QMutableMapIterator>
15#include <QRegularExpression>
19#include <QXmlStreamReader>
23#include <xercesc/framework/MemBufInputSource.hpp>
24#include <xercesc/framework/XMLGrammarPoolImpl.hpp>
26#include <xercesc/parsers/SAX2XMLReaderImpl.hpp>
28#include <xercesc/sax/ErrorHandler.hpp>
29#include <xercesc/sax/SAXParseException.hpp>
31#include <xercesc/util/PlatformUtils.hpp>
32#include <xercesc/util/XMLString.hpp>
33#include <xercesc/util/XMLUni.hpp>
35#include <xercesc/framework/XMLGrammarPoolImpl.hpp>
36#include <xercesc/validators/common/Grammar.hpp>
38using namespace xercesc;
57class CustomErrorHandler :
public ErrorHandler
64 CustomErrorHandler(
QString *messages)
65 : m_messages(messages)
82 enum severity { s_warning, s_error, s_fatal };
88 void warning(
const SAXParseException &e)
override
98 void error(
const SAXParseException &e)
override
108 void fatalError(
const SAXParseException &e)
override
117 void resetErrors()
override
127 void handle(
const SAXParseException &e, severity s)
130 const XMLCh *xid(e.getPublicId());
132 xid = e.getSystemId();
134 m_messages <<
QString::fromUtf16(xid) <<
":" << e.getLineNumber() <<
":" << e.getColumnNumber() <<
" " << (s == s_warning ?
"warning: " :
"error: ")
147 bool m_failed =
false;
150class CustomXMLValidator :
public SAX2XMLReaderImpl
154 CustomErrorHandler eh{&messages};
156 CustomXMLValidator(XMLGrammarPool *xsd)
157 : SAX2XMLReaderImpl(XMLPlatformUtils::fgMemoryManager, xsd)
161 setFeature(XMLUni::fgSAX2CoreNameSpaces,
true);
162 setFeature(XMLUni::fgSAX2CoreNameSpacePrefixes,
true);
163 setFeature(XMLUni::fgSAX2CoreValidation,
true);
167 setFeature(XMLUni::fgXercesSchema,
true);
168 setFeature(XMLUni::fgXercesSchemaFullChecking,
true);
169 setFeature(XMLUni::fgXercesValidationErrorAsFatal,
true);
173 setFeature(XMLUni::fgXercesUseCachedGrammarInParse,
true);
178 setFeature(XMLUni::fgXercesLoadSchema,
false);
183 setFeature(XMLUni::fgXercesHandleMultipleImports,
true);
185 setErrorHandler(&eh);
191#include "../lib/worddelimiters_p.h"
192#include "../lib/xml_p.h"
196using KSyntaxHighlighting::WordDelimiters;
197using KSyntaxHighlighting::Xml::attrToBool;
201static constexpr QStringView operator""_sv(
const char16_t *s, std::size_t n)
213 KateVersion(
int majorRevision = 0,
int minorRevision = 0)
214 : majorRevision(majorRevision)
215 , minorRevision(minorRevision)
219 bool operator<(
const KateVersion &version)
const
221 return majorRevision <
version.majorRevision || (majorRevision ==
version.majorRevision && minorRevision <
version.minorRevision);
230 m_currentDefinition = &*m_definitions.insert(name, Definition{});
231 m_currentDefinition->languageName =
name;
232 m_currentDefinition->filename = filename;
233 m_currentDefinition->kateVersionStr = verStr.
toString();
234 m_currentKeywords =
nullptr;
235 m_currentContext =
nullptr;
237 const auto idx = verStr.
indexOf(u
'.');
239 qWarning() << filename <<
"invalid kateversion" << verStr;
245 auto checkName = [
this, &filename](
char const *nameType,
const QString &
name) {
246 auto it = m_names.find(name);
247 if (it != m_names.end()) {
248 qWarning() << filename <<
"duplicate" << nameType <<
"with" << it.value();
251 m_names.insert(name, filename);
254 checkName(
"name", name);
255 for (
const auto &alternativeName : alternativeNames) {
256 checkName(
"alternative name", alternativeName);
260 KateVersion currentVersion()
const
262 return m_currentDefinition->kateVersion;
269 if (m_currentContext) {
270 m_currentContext->rules.push_back(Context::Rule{});
271 auto &rule = m_currentContext->rules.back();
272 m_success = rule.parseElement(m_currentDefinition->filename, xml) && m_success;
273 m_currentContext->hasDynamicRule = m_currentContext->hasDynamicRule || rule.dynamic == XmlBool::True;
274 }
else if (m_currentKeywords) {
275 m_inKeywordItem =
true;
276 }
else if (xml.
name() == u
"context"_sv) {
277 processContextElement(xml);
278 }
else if (xml.
name() == u
"list"_sv) {
279 processListElement(xml);
280 }
else if (xml.
name() == u
"keywords"_sv) {
281 m_success = m_currentDefinition->parseKeywords(xml) && m_success;
282 }
else if (xml.
name() == u
"emptyLine"_sv) {
283 m_success = parseEmptyLine(m_currentDefinition->filename, xml) && m_success;
284 }
else if (xml.
name() == u
"itemData"_sv) {
285 m_success = m_currentDefinition->itemDatas.parseElement(m_currentDefinition->filename, xml) && m_success;
290 if (m_currentContext && xml.
name() == u
"context"_sv) {
291 m_currentContext =
nullptr;
292 }
else if (m_currentKeywords && xml.
name() == u
"list"_sv) {
293 m_currentKeywords =
nullptr;
294 }
else if (m_currentKeywords) {
295 m_success = m_currentKeywords->items.parseElement(m_currentDefinition->filename, xml, m_textContent) && m_success;
296 m_textContent.clear();
297 m_inKeywordItem =
false;
303 if (m_inKeywordItem) {
304 m_textContent += xml.
text();
313 void resolveContexts()
316 while (def.hasNext()) {
318 auto &definition = def.value();
319 auto &contexts = definition.contexts;
321 if (contexts.isEmpty()) {
322 qWarning() << definition.filename <<
"has no context";
327 auto markAsUsedContext = [](ContextName &contextName) {
328 if (!contextName.stay && contextName.context) {
329 contextName.context->isOnlyIncluded =
false;
334 while (contextIt.hasNext()) {
336 auto &context = contextIt.value();
337 resolveContextName(definition, context, context.lineEndContext, context.line);
338 resolveContextName(definition, context, context.lineEmptyContext, context.line);
339 resolveContextName(definition, context, context.fallthroughContext, context.line);
340 markAsUsedContext(context.lineEndContext);
341 markAsUsedContext(context.lineEmptyContext);
342 markAsUsedContext(context.fallthroughContext);
343 for (
auto &rule : context.rules) {
344 rule.parentContext = &context;
345 resolveContextName(definition, context, rule.context, rule.line);
346 if (rule.type != Context::Rule::Type::IncludeRules) {
347 markAsUsedContext(rule.context);
348 }
else if (rule.includeAttrib == XmlBool::True && rule.context.context) {
349 rule.context.context->referencedWithIncludeAttrib =
true;
354 auto *firstContext = &*definition.contexts.find(definition.firstContextName);
355 firstContext->isOnlyIncluded =
false;
356 definition.firstContext = firstContext;
359 resolveIncludeRules();
364 bool success = m_success;
366 const auto usedContexts = extractUsedContexts();
372 while (def.hasNext()) {
374 const auto &definition = def.value();
375 const auto &filename = definition.filename;
377 auto *maxDef = maxKateVersionDefinition(definition, maxVersionByDefinitions);
378 if (maxDef != &definition) {
379 qWarning() << definition.filename <<
"depends on a language" << maxDef->languageName <<
"in version" << maxDef->kateVersionStr
380 <<
". Please, increase kateversion.";
386 success = checkKeywordsList(definition) && success;
387 success = checkContexts(definition, usedAttributeNames, ignoredAttributeNames, usedContexts, unreachableIncludedRules) && success;
390 const auto invalidNames = usedAttributeNames - definition.itemDatas.styleNames;
391 for (
const auto &styleName : invalidNames) {
392 qWarning() << filename <<
"line" << styleName.line <<
"reference of non-existing itemData attributes:" << styleName.name;
397 const auto ignoredNames = ignoredAttributeNames - usedAttributeNames;
398 for (
const auto &styleName : ignoredNames) {
399 qWarning() << filename <<
"line" << styleName.line <<
"attribute" << styleName.name
400 <<
"is never used. All uses are with lookAhead=true or <IncludeRules/>";
405 auto unusedNames = definition.itemDatas.styleNames - usedAttributeNames;
406 unusedNames -= ignoredNames;
407 for (
const auto &styleName : std::as_const(unusedNames)) {
408 qWarning() << filename <<
"line" << styleName.line <<
"unused itemData:" << styleName.name;
414 while (unreachableIncludedRuleIt.hasNext()) {
415 unreachableIncludedRuleIt.next();
416 IncludedRuleUnreachableBy &unreachableRulesBy = unreachableIncludedRuleIt.value();
417 if (unreachableRulesBy.alwaysUnreachable) {
418 auto *rule = unreachableIncludedRuleIt.key();
420 if (!rule->parentContext->isOnlyIncluded) {
426 auto &unreachableBy = unreachableRulesBy.unreachableBy;
427 unreachableBy.
erase(std::remove_if(unreachableBy.begin(),
429 [&](
const RuleAndInclude &ruleAndInclude) {
430 if (rules.contains(ruleAndInclude.rule)) {
433 rules.
insert(ruleAndInclude.rule);
436 unreachableBy.end());
440 for (
auto &ruleAndInclude : std::as_const(unreachableBy)) {
441 message += u
"line "_sv;
444 message += ruleAndInclude.rule->parentContext->name;
445 if (rule->filename != ruleAndInclude.rule->filename) {
447 message += ruleAndInclude.rule->filename;
450 if (ruleAndInclude.includeRules) {
451 message += u
" via line "_sv;
454 message += u
"], "_sv;
458 qWarning() << rule->filename <<
"line" << rule->line <<
"no IncludeRule can reach this rule, hidden by" << message;
480 Context *context =
nullptr;
493 if (attr.
name() != attrName) {
499 qWarning() << filename <<
"line" << xml.
lineNumber() << attrName <<
"attribute is empty";
508 bool extractXmlBool(XmlBool &xmlBool,
QStringView attrName)
510 if (attr.
name() != attrName) {
514 xmlBool = attr.
value().
isNull() ? XmlBool::Unspecified : attrToBool(attr.
value()) ? XmlBool::True : XmlBool::False;
521 bool extractPositive(
int &positive,
QStringView attrName)
523 if (attr.
name() != attrName) {
530 if (!ok || positive < 0) {
531 qWarning() << filename <<
"line" << xml.
lineNumber() << attrName <<
"should be a positive integer:" << attr.
value();
542 if (attr.
name() != attrName) {
546 const auto value = attr.
value();
547 if (value.isEmpty() ) {
548 qWarning() << filename <<
"line" << xml.
lineNumber() << attrName <<
"should be a color:" << value;
559 if (attr.
name() != attrName) {
567 qWarning() << filename <<
"line" << xml.
lineNumber() << attrName <<
"must contain exactly one char:" << attr.
value();
575 bool checkIfExtracted(
bool isExtracted)
581 qWarning() << filename <<
"line" << xml.
lineNumber() <<
"unknown attribute:" << attr.
name();
592 friend size_t qHash(
const Item &item,
size_t seed = 0)
594 return qHash(item.content, seed);
599 return item0.content == item1.content;
613 qWarning() << filename <<
"line" << line <<
"is empty:" << xml.
name();
617 if (xml.
name() == u
"include"_sv) {
618 includes.
insert({content, line});
619 }
else if (xml.
name() == u
"item"_sv) {
620 keywords.
append({content, line});
622 qWarning() << filename <<
"line" << line <<
"invalid element:" << xml.
name();
640 for (
const auto &attr : attrs) {
641 Parser parser{filename, xml, attr, success};
643 const bool isExtracted = parser.extractString(name, u
"name"_sv);
645 success = parser.checkIfExtracted(isExtracted);
677 bool isDotRegex =
false;
687 XmlBool firstNonSpace{};
690 XmlBool insensitive{};
699 XmlBool includeAttrib{};
721 Context
const *parentContext =
nullptr;
727 this->filename = filename;
730 using Pair = QPair<QStringView, Type>;
731 static const auto pairs = {
732 Pair{u
"AnyChar"_sv, Type::AnyChar},
733 Pair{u
"Detect2Chars"_sv, Type::Detect2Chars},
734 Pair{u
"DetectChar"_sv, Type::DetectChar},
735 Pair{u
"DetectIdentifier"_sv, Type::DetectIdentifier},
736 Pair{u
"DetectSpaces"_sv, Type::DetectSpaces},
737 Pair{u
"Float"_sv, Type::Float},
738 Pair{u
"HlCChar"_sv, Type::HlCChar},
739 Pair{u
"HlCHex"_sv, Type::HlCHex},
740 Pair{u
"HlCOct"_sv, Type::HlCOct},
741 Pair{u
"HlCStringChar"_sv, Type::HlCStringChar},
742 Pair{u
"IncludeRules"_sv, Type::IncludeRules},
743 Pair{u
"Int"_sv, Type::Int},
744 Pair{u
"LineContinue"_sv, Type::LineContinue},
745 Pair{u
"RangeDetect"_sv, Type::RangeDetect},
746 Pair{u
"RegExpr"_sv, Type::RegExpr},
747 Pair{u
"StringDetect"_sv, Type::StringDetect},
748 Pair{u
"WordDetect"_sv, Type::WordDetect},
749 Pair{u
"keyword", Type::keyword},
752 for (
auto pair : pairs) {
755 bool success = parseAttributes(filename, xml);
756 success = checkMandoryAttributes(filename, xml) && success;
757 if (success && type == Type::RegExpr) {
759 static const QRegularExpression isDot(QStringLiteral(R
"(^\(?\.(?:[*+][*+?]?|[*+]|\{1\})?\$?$)"));
761 static const QRegularExpression removeParentheses(QStringLiteral(R
"(\((?:\?:)?|\))"));
764 isDotRegex = reg.contains(isDot);
767 static const QRegularExpression allSuffix(QStringLiteral(
"(?<!\\\\)[.][*][?+]?[$]?$"));
768 sanitizedString = string;
771 if (sanitizedString.
isEmpty() || sanitizedString == u
"^"_sv) {
772 sanitizedString = string;
779 qWarning() << filename <<
"line" << xml.
lineNumber() <<
"unknown element:" << xml.
name();
789 for (
const auto &attr : attrs) {
790 Parser parser{filename, xml, attr, success};
793 const bool isExtracted
794 = parser.extractString(attribute, u
"attribute"_sv)
795 || parser.extractString(context.name, u
"context"_sv)
796 || parser.extractXmlBool(lookAhead, u
"lookAhead"_sv)
797 || parser.extractXmlBool(firstNonSpace, u
"firstNonSpace"_sv)
798 || parser.extractString(beginRegion, u
"beginRegion"_sv)
799 || parser.extractString(endRegion, u
"endRegion"_sv)
800 || parser.extractPositive(column, u
"column"_sv)
801 || ((
type == Type::RegExpr
802 ||
type == Type::StringDetect
803 ||
type == Type::WordDetect
804 ||
type == Type::keyword
805 ) && parser.extractXmlBool(insensitive, u
"insensitive"_sv))
806 || ((
type == Type::DetectChar
807 ||
type == Type::RegExpr
808 ||
type == Type::StringDetect
809 ||
type == Type::keyword
810 ) && parser.extractXmlBool(dynamic, u
"dynamic"_sv))
811 || ((
type == Type::RegExpr)
812 && parser.extractXmlBool(minimal, u
"minimal"_sv))
813 || ((
type == Type::DetectChar
814 ||
type == Type::Detect2Chars
815 ||
type == Type::LineContinue
816 ||
type == Type::RangeDetect
817 ) && parser.extractChar(char0, u
"char"_sv))
818 || ((
type == Type::Detect2Chars
819 ||
type == Type::RangeDetect
820 ) && parser.extractChar(char1, u
"char1"_sv))
821 || ((
type == Type::AnyChar
822 ||
type == Type::RegExpr
823 ||
type == Type::StringDetect
824 ||
type == Type::WordDetect
825 ||
type == Type::keyword
826 ) && parser.extractString(
string, u
"String"_sv))
827 || ((
type == Type::IncludeRules)
828 && parser.extractXmlBool(includeAttrib, u
"includeAttrib"_sv))
829 || ((
type == Type::Float
830 ||
type == Type::HlCHex
831 ||
type == Type::HlCOct
833 ||
type == Type::keyword
834 ||
type == Type::WordDetect
835 ) && (parser.extractString(additionalDeliminator, u
"additionalDeliminator"_sv)
836 || parser.extractString(weakDeliminator, u
"weakDeliminator"_sv)))
840 success = parser.checkIfExtracted(isExtracted);
843 if (type == Type::LineContinue && char0 == u
'\0') {
860 case Type::StringDetect:
861 case Type::WordDetect:
863 missingAttr =
string.
isEmpty() ? QStringLiteral(
"String") :
QString();
866 case Type::DetectChar:
867 missingAttr = !char0.
unicode() ? QStringLiteral(
"char") :
QString();
870 case Type::Detect2Chars:
871 case Type::RangeDetect:
872 missingAttr = !char0.
unicode() && !char1.
unicode() ? QStringLiteral(
"char and char1")
873 : !char0.
unicode() ? QStringLiteral(
"char")
874 : !char1.
unicode() ? QStringLiteral(
"char1")
878 case Type::IncludeRules:
879 missingAttr = context.name.isEmpty() ? QStringLiteral(
"context") :
QString();
882 case Type::DetectIdentifier:
883 case Type::DetectSpaces:
888 case Type::HlCStringChar:
890 case Type::LineContinue:
895 qWarning() << filename <<
"line" << xml.
lineNumber() <<
"missing attribute:" << missingAttr;
905 bool isOnlyIncluded =
true;
907 bool referencedWithIncludeAttrib =
false;
908 bool hasDynamicRule =
false;
911 ContextName lineEndContext;
912 ContextName lineEmptyContext;
913 ContextName fallthroughContext;
916 XmlBool fallthrough{};
917 XmlBool stopEmptyLineContextSwitchLoop{};
926 for (
const auto &attr : attrs) {
927 Parser parser{filename, xml, attr, success};
928 XmlBool noIndentationBasedFolding{};
931 const bool isExtracted = parser.extractString(name, u
"name"_sv)
932 || parser.extractString(attribute, u
"attribute"_sv)
933 || parser.extractString(lineEndContext.name, u
"lineEndContext"_sv)
934 || parser.extractString(lineEmptyContext.name, u
"lineEmptyContext"_sv)
935 || parser.extractString(fallthroughContext.name, u
"fallthroughContext"_sv)
936 || parser.extractXmlBool(dynamic, u
"dynamic"_sv)
937 || parser.extractXmlBool(fallthrough, u
"fallthrough"_sv)
938 || parser.extractXmlBool(stopEmptyLineContextSwitchLoop, u
"stopEmptyLineContextSwitchLoop"_sv)
939 || parser.extractXmlBool(noIndentationBasedFolding, u
"noIndentationBasedFolding"_sv);
942 success = parser.checkIfExtracted(isExtracted);
946 qWarning() << filename <<
"line" << xml.
lineNumber() <<
"missing attribute: name";
951 qWarning() << filename <<
"line" << xml.
lineNumber() <<
"missing attribute: attribute";
964 friend size_t qHash(
const Style &style,
size_t seed = 0)
966 return qHash(style.name, seed);
971 return style0.name == style1.name;
986 for (
const auto &attr : attrs) {
987 Parser parser{filename, xml, attr, success};
990 const bool isExtracted
991 = parser.extractString(name, u
"name"_sv)
992 || parser.extractString(defStyleNum, u
"defStyleNum"_sv)
993 || parser.extractXmlBool(
boolean, u
"bold"_sv)
994 || parser.extractXmlBool(
boolean, u
"italic"_sv)
995 || parser.extractXmlBool(
boolean, u
"underline"_sv)
996 || parser.extractXmlBool(
boolean, u
"strikeOut"_sv)
997 || parser.extractXmlBool(
boolean, u
"spellChecking"_sv)
998 || parser.checkColor(u
"color"_sv)
999 || parser.checkColor(u
"selColor"_sv)
1000 || parser.checkColor(u
"backgroundColor"_sv)
1001 || parser.checkColor(u
"selBackgroundColor"_sv);
1004 success = parser.checkIfExtracted(isExtracted);
1008 const auto len = styleNames.
size();
1010 if (len == styleNames.
size()) {
1011 qWarning() << filename <<
"line" << xml.
lineNumber() <<
"itemData duplicate:" <<
name;
1023 ItemDatas itemDatas;
1025 const Context *firstContext =
nullptr;
1027 WordDelimiters wordDelimiters;
1028 KateVersion kateVersion{};
1036 wordDelimiters.append(xml.
attributes().
value(u
"additionalDeliminator"_sv));
1046 m_success = context.parseElement(m_currentDefinition->filename, xml) && m_success;
1047 if (m_currentDefinition->firstContextName.isEmpty()) {
1048 m_currentDefinition->firstContextName = context.name;
1050 if (m_currentDefinition->contexts.contains(context.name)) {
1051 qWarning() << m_currentDefinition->filename <<
"line" << xml.
lineNumber() <<
"duplicate context:" << context.name;
1054 m_currentContext = &*m_currentDefinition->contexts.insert(context.name, context);
1061 m_success = keywords.parseElement(m_currentDefinition->filename, xml) && m_success;
1062 if (m_currentDefinition->keywordsList.contains(keywords.name)) {
1063 qWarning() << m_currentDefinition->filename <<
"line" << xml.
lineNumber() <<
"duplicate list:" << keywords.name;
1066 m_currentKeywords = &*m_currentDefinition->keywordsList.insert(keywords.name, keywords);
1071 auto it = maxVersionByDefinitions.
find(&definition);
1072 if (it != maxVersionByDefinitions.
end()) {
1075 auto it = maxVersionByDefinitions.
insert(&definition, &definition);
1076 for (
const auto &referencedDef : definition.referencedDefinitions) {
1077 auto *maxDef = maxKateVersionDefinition(*referencedDef, maxVersionByDefinitions);
1078 if (it.value()->kateVersion < maxDef->kateVersion) {
1079 it.value() = maxDef;
1087 void resolveIncludeRules()
1093 while (def.hasNext()) {
1095 auto &definition = def.value();
1097 while (contextIt.hasNext()) {
1099 auto ¤tContext = contextIt.value();
1100 for (
auto &rule : currentContext.rules) {
1101 if (rule.type != Context::Rule::Type::IncludeRules) {
1105 if (rule.context.stay) {
1106 qWarning() << definition.filename <<
"line" << rule.line <<
"IncludeRules refers to himself";
1111 if (rule.context.popCount) {
1112 qWarning() << definition.filename <<
"line" << rule.line <<
"IncludeRules with #pop prefix";
1116 if (!rule.context.context) {
1123 usedContexts.
clear();
1124 usedContexts.
insert(rule.context.context);
1126 contexts.
append(rule.context.context);
1128 for (
int i = 0; i < contexts.
size(); ++i) {
1129 currentContext.hasDynamicRule = contexts[i]->hasDynamicRule;
1130 for (
const auto &includedRule : contexts[i]->rules) {
1131 if (includedRule.type != Context::Rule::Type::IncludeRules) {
1132 rule.includedRules.append(&includedRule);
1133 }
else if (&rule == &includedRule) {
1134 qWarning() << definition.filename <<
"line" << rule.line <<
"IncludeRules refers to himself by recursivity";
1137 rule.includedIncludeRules.insert(&includedRule);
1139 if (includedRule.includedRules.isEmpty()) {
1140 const auto *context = includedRule.context.context;
1141 if (context && !usedContexts.
contains(context)) {
1142 contexts.
append(context);
1143 usedContexts.
insert(context);
1146 rule.includedRules.append(includedRule.includedRules);
1164 while (def.hasNext()) {
1166 const auto &definition = def.value();
1168 if (definition.firstContext) {
1169 usedContexts.
insert(definition.firstContext);
1171 contexts.
append(definition.firstContext);
1173 for (
int i = 0; i < contexts.
size(); ++i) {
1174 auto appendContext = [&](
const Context *context) {
1175 if (context && !usedContexts.
contains(context)) {
1176 contexts.
append(context);
1177 usedContexts.
insert(context);
1181 const auto *context = contexts[i];
1182 appendContext(context->lineEndContext.context);
1183 appendContext(context->lineEmptyContext.context);
1184 appendContext(context->fallthroughContext.context);
1186 for (
auto &rule : context->rules) {
1187 appendContext(rule.context.context);
1193 return usedContexts;
1196 struct RuleAndInclude {
1197 const Context::Rule *rule;
1198 const Context::Rule *includeRules;
1200 explicit operator bool()
const
1206 struct IncludedRuleUnreachableBy {
1208 bool alwaysUnreachable =
true;
1212 bool checkContexts(
const Definition &definition,
1218 bool success =
true;
1221 while (contextIt.hasNext()) {
1224 const auto &context = contextIt.value();
1225 const auto &filename = definition.filename;
1227 if (!usedContexts.
contains(&context)) {
1228 qWarning() << filename <<
"line" << context.line <<
"unused context:" << context.name;
1233 if (context.name.startsWith(u
"#pop"_sv)) {
1234 qWarning() << filename <<
"line" << context.line <<
"the context name must not start with '#pop':" << context.name;
1238 if (!context.attribute.isEmpty() && (!context.isOnlyIncluded || context.referencedWithIncludeAttrib)) {
1239 usedAttributeNames.
insert({context.attribute, context.line});
1242 success = checkContextAttribute(definition, context) && success;
1243 success = checkUreachableRules(definition.filename, context, unreachableIncludedRules) && success;
1244 success = suggestRuleMerger(definition.filename, context) && success;
1246 for (
const auto &rule : context.rules) {
1247 if (!rule.attribute.isEmpty()) {
1248 if (rule.lookAhead != XmlBool::True) {
1249 usedAttributeNames.
insert({rule.attribute, rule.line});
1251 ignoredAttributeNames.
insert({rule.attribute, rule.line});
1254 success = checkLookAhead(rule) && success;
1255 success = checkStringDetect(rule) && success;
1256 success = checkWordDetect(rule) && success;
1257 success = checkKeyword(definition, rule) && success;
1258 success = checkRegExpr(filename, rule, context) && success;
1259 success = checkDelimiters(definition, rule) && success;
1275 bool checkRegExpr(
const QString &filename,
const Context::Rule &rule,
const Context &context)
const
1278 if (rule.type == Context::Rule::Type::RegExpr && !rule.string.isEmpty()) {
1280 if (!checkRegularExpression(rule.filename, regexp, rule.line)) {
1285 if (rule.dynamic == XmlBool::True) {
1287 if (!rule.string.contains(placeHolder)) {
1288 qWarning() << rule.filename <<
"line" << rule.line <<
"broken regex:" << rule.string <<
"problem: dynamic=true but no %\\d+ placeholder";
1293 if (rule.lookAhead == XmlBool::True && (rule.string.endsWith(u
".*$"_sv) || rule.string.endsWith(u
".*"_sv)) && -1 == rule.string.indexOf(u
'|')) {
1294 qWarning() << rule.filename <<
"line" << rule.line <<
"RegExpr with lookAhead=1 doesn't need to end with '.*' or '.*$':" << rule.string;
1298 auto reg = (rule.lookAhead == XmlBool::True) ? rule.sanitizedString : rule.string;
1299 if (rule.lookAhead == XmlBool::True) {
1301 R
"(((?<!\\)\\(?:[DSWdsw]|x[0-9a-fA-F]{2}|x\{[0-9a-fA-F]+\}|0\d\d|o\{[0-7]+\}|u[0-9a-fA-F]{4})|(?<!\\)[^])}\\]|(?=\\)\\\\)[*][?+]?$)"));
1302 reg.replace(removeAllSuffix, QString());
1311 QStringLiteral(R
"(^\^?(?:\((?:\?:)?)?\^?(?:\\s|\[(?:\\s| (?:\t|\\t)|(?:\t|\\t) )\])\)?(?:[*+][*+?]?|[*+])?\)?\)?$)"));
1312 if (rule.string.contains(isDetectSpaces)) {
1313 char const *extraMsg = rule.string.contains(u
'^') ?
"+ column=\"0\" or firstNonSpace=\"1\"" :
"";
1314 qWarning() << rule.filename <<
"line" << rule.line <<
"RegExpr should be replaced by DetectSpaces / DetectChar / AnyChar" << extraMsg <<
":"
1319#define REG_ESCAPE_CHAR R"(\\(?:[^0BDPSWbdpswoux]|x[0-9a-fA-F]{2}|x\{[0-9a-fA-F]+\}|0\d\d|o\{[0-7]+\}|u[0-9a-fA-F]{4}))"
1320#define REG_CHAR "(?:" REG_ESCAPE_CHAR "|\\[(?:" REG_ESCAPE_CHAR "|.)\\]|[^[.^])"
1324 "\\.\\*[?+]?" REG_CHAR
"|"
1325 "\\[\\^(" REG_ESCAPE_CHAR
"|.)\\]\\*[?+]?\\1"
1327 if ((rule.lookAhead == XmlBool::True || rule.minimal == XmlBool::True || rule.string.contains(u
".*?"_sv) || rule.string.contains(u
"[^"_sv))
1328 && reg.contains(isRange)) {
1329 qWarning() << rule.filename <<
"line" << rule.line <<
"RegExpr should be replaced by RangeDetect:" << rule.string;
1334 static const QRegularExpression isAnyChar(QStringLiteral(R
"(^(\^|\((\?:)?)*\[(?!\^)[-\]]?(\\[^0BDPSWbdpswoux]|[^-\]\\])*\]\)*$)"));
1335 if (rule.string.contains(isAnyChar)) {
1336 auto extra = (reg[0] == u
'^' || reg[1] == u
'^') ?
"with column=\"0\"" :
"";
1337 qWarning() << rule.filename <<
"line" << rule.line <<
"RegExpr should be replaced by AnyChar:" << rule.string << extra;
1342 static const QRegularExpression isLineContinue(QStringLiteral(
"^\\^?" REG_CHAR
"\\$$"));
1343 if (reg.contains(isLineContinue)) {
1344 auto extra = (reg[0] == u
'^') ?
"with column=\"0\"" :
"";
1345 qWarning() << rule.filename <<
"line" << rule.line <<
"RegExpr should be replaced by LineContinue:" << rule.string << extra;
1349#define REG_DIGIT uR"((\[(0-9|\\d)\]|\\d))"
1350#define REG_DIGITS REG_DIGIT u"([+]|" REG_DIGIT u"[*])"
1351#define REG_DOT uR"((\\[.]|\[.\]))"
1353 static const QRegularExpression isInt(uR
"(^(\((\?:)?)*\\b(\((\?:)?)*)" REG_DIGITS uR"(\)*$)"_s);
1354 if (reg.contains(isInt)) {
1355 qWarning() << rule.filename <<
"line" << rule.line <<
"RegExpr should be replaced by Int:" << rule.string;
1361 uR
"(^(\\b|\((\?:)?)*)" REG_DIGITS REG_DOT
1362 REG_DIGIT u"[*][|]" REG_DOT REG_DIGITS uR
"(\)+\((\?:)?\[[eE]+\]\[(\\?-\\?\+|\\?\+\\?-)\]\?)" REG_DIGITS uR"(\)\?\)*$)"_s);
1363 if (reg.contains(isFloat)) {
1364 qWarning() << rule.filename <<
"line" << rule.line <<
"RegExpr should be replaced by Float:" << rule.string;
1373 reg.replace(sanitize1, QStringLiteral(
"_"));
1376#undef REG_ESCAPE_CHAR
1379 static const QRegularExpression isMinimal(QStringLiteral(
"(?![.][*+?][$]?[)]*$)[.][*+?][^?+]"));
1382 if (rule.lookAhead == XmlBool::True && rule.minimal != XmlBool::True && reg.contains(isMinimal) && !reg.contains(hasNotGreedy)
1383 && (!rule.context.context || !rule.context.context->hasDynamicRule || regexp.captureCount() == 0)
1384 && (reg.back() != u
'$' || reg.contains(u
'|'))) {
1385 qWarning() << rule.filename <<
"line" << rule.line
1386 <<
"RegExpr should be have minimal=\"1\" or use lazy operator (i.g, '.*' -> '.*?'):" << rule.string;
1392 reg.replace(sanitize2, QStringLiteral("___"));
1395 static const QRegularExpression sanitize3(QStringLiteral(R
"(\[(?:\^\]?[^]]*|\]?[^]\\]*?\\.[^]]*|\][^]]{2,}|[^]]{3,})\]|(\[\]?[^]]*\]))"));
1396 reg.replace(sanitize3, QStringLiteral("...\\1"));
1400 reg.replace(sanitize4, QStringLiteral("_"));
1402 const int len = reg.size();
1404 static const QRegularExpression toInsensitive(QStringLiteral(R
"(\[(?:([^]])\1)\])"));
1405 reg = reg.toUpper();
1406 reg.replace(toInsensitive, QString());
1410 static const QRegularExpression isStringDetect(QStringLiteral(R
"(^\^?(?:[^|\\?*+$^[{(.]|{(?!\d+,\d*}|,\d+})|\(\?:)+$)"));
1411 if (reg.contains(isStringDetect)) {
1412 char const *extraMsg = rule.string.contains(u
'^') ?
"+ column=\"0\" or firstNonSpace=\"1\"" :
"";
1413 qWarning() << rule.filename <<
"line" << rule.line <<
"RegExpr should be replaced by StringDetect / Detect2Chars / DetectChar" << extraMsg
1414 <<
":" << rule.string;
1415 if (len != reg.size()) {
1416 qWarning() << rule.filename <<
"line" << rule.line <<
"insensitive=\"1\" missing:" << rule.string;
1422 if (rule.column == -1) {
1427 auto first = std::as_const(reg).begin();
1428 auto last = std::as_const(reg).end();
1431 while (u
'(' == *first) {
1434 if (u
'?' == *first || u
':' == first[1]) {
1439 if (u
'^' == *first) {
1440 const int bolDepth = depth;
1443 while (++first != last) {
1444 if (u
'(' == *first) {
1446 }
else if (u
')' == *first) {
1448 if (depth < bolDepth) {
1450 if (first + 1 != last && u
"*?"_sv.contains(first[1])) {
1455 }
else if (u
'|' == *first) {
1457 if (depth <= bolDepth) {
1465 qWarning() << rule.filename <<
"line" << rule.line <<
"column=\"0\" missing with RegExpr:" << rule.string;
1472 if (rule.column == 0 && !rule.isDotRegex) {
1473 bool hasStartOfLine =
false;
1474 auto first = std::as_const(reg).begin();
1475 auto last = std::as_const(reg).end();
1476 for (; first != last; ++first) {
1477 if (*first == u
'^') {
1478 hasStartOfLine =
true;
1480 }
else if (*first == u
'(') {
1481 if (last - first >= 3 && first[1] == u
'?' && first[2] == u
':') {
1489 if (!hasStartOfLine) {
1490 qWarning() << rule.filename <<
"line" << rule.line
1491 <<
"start of line missing in the pattern with column=\"0\" (i.e. abc -> ^abc):" << rule.string;
1496 bool useCapture =
false;
1499 if (regexp.captureCount()) {
1502 while (maxCapture && !s.contains(referenceNames[maxCapture - 1])) {
1508 int maxCaptureUsed = 0;
1510 if (rule.context.context && !rule.context.stay) {
1511 for (
const auto &nextRule : std::as_const(rule.context.context->rules)) {
1512 if (nextRule.dynamic == XmlBool::True) {
1524 int maxDynamicCapture = maximalCapture(cap, nextRule.string);
1525 maxCaptureUsed = std::max(maxCaptureUsed, maxDynamicCapture);
1552 const int maxBackReference = std::max(maximalCapture(num1, rule.string), maximalCapture(num2, rule.string));
1554 const int maxCapture = std::max(maxCaptureUsed, maxBackReference);
1556 if (maxCapture && regexp.captureCount() > maxCapture) {
1557 qWarning() << rule.filename <<
"line" << rule.line <<
"RegExpr with" << regexp.captureCount() <<
"captures but only" << maxCapture
1558 <<
"are used. Please, replace '(...)' with '(?:...)':" << rule.string;
1562 useCapture = maxCapture;
1568 QStringLiteral(R
"(^(\((\?:)?|\^)*\[(\\p\{L\}|_){2}\]([+][?+]?)?\[(\\p\{N\}|\\p\{L\}|_){3}\][*][?+]?\)*$)"));
1569 if (rule.string.contains(isDetectIdentifier)) {
1570 qWarning() << rule.filename <<
"line" << rule.line <<
"RegExpr should be replaced by DetectIdentifier:" << rule.string;
1575 if (rule.isDotRegex) {
1577 int i = &rule - context.rules.data() + 1;
1578 const bool hasColumn = (rule.column != -1);
1579 const bool hasFirstNonSpace = (rule.firstNonSpace == XmlBool::True);
1580 const bool isSpecial = (hasColumn || hasFirstNonSpace);
1581 for (; i < context.rules.size(); ++i) {
1582 auto &rule2 = context.rules[i];
1583 if (rule2.type == Context::Rule::Type::IncludeRules && isSpecial) {
1584 i = context.rules.size();
1588 const bool hasColumn2 = (rule2.column != -1);
1589 const bool hasFirstNonSpace2 = (rule2.firstNonSpace == XmlBool::True);
1590 if ((!isSpecial && !hasColumn2 && !hasFirstNonSpace2) || (hasColumn && rule.column == rule2.column)
1591 || (hasFirstNonSpace && hasFirstNonSpace2)) {
1596 auto ruleFilename = (filename == rule.filename) ?
QString() : u
"in "_sv + rule.filename;
1597 if (i == context.rules.size()) {
1598 if (rule.lookAhead == XmlBool::True && rule.firstNonSpace != XmlBool::True && rule.column == -1 && rule.beginRegion.isEmpty()
1599 && rule.endRegion.isEmpty() && !useCapture) {
1600 qWarning() << filename <<
"context line" << context.line <<
": RegExpr line" << rule.line << ruleFilename
1601 <<
"should be replaced by fallthroughContext:" << rule.string;
1604 auto &nextRule = context.rules[i];
1605 auto nextRuleFilename = (filename == nextRule.filename) ?
QString() : u
"in "_sv + nextRule.filename;
1606 qWarning() << filename <<
"context line" << context.line <<
"contains unreachable element line" << nextRule.line << nextRuleFilename
1607 <<
"because a dot RegExpr is used line" << rule.line << ruleFilename;
1611 static const QRegularExpression unnecessaryQuantifier1(QStringLiteral(R
"([*+?]([.][*+?]{0,2})?$)"));
1612 static const QRegularExpression unnecessaryQuantifier2(QStringLiteral(R
"([*+?]([.][*+?]{0,2})?[)]*$)"));
1613 auto &unnecessaryQuantifier = useCapture ? unnecessaryQuantifier1 : unnecessaryQuantifier2;
1614 if (rule.lookAhead == XmlBool::True && rule.minimal != XmlBool::True && reg.contains(unnecessaryQuantifier)) {
1615 qWarning() << rule.filename <<
"line" << rule.line
1616 <<
"Last quantifier is not necessary (i.g., 'xyz*' -> 'xy', 'xyz+.' -> 'xyz.'):" << rule.string;
1628 bool success =
true;
1631 XmlBool casesensitive{};
1634 for (
auto &attr : attrs) {
1635 Parser parser{filename, xml, attr, success};
1637 const bool isExtracted = parser.extractString(pattern, u
"regexpr"_sv) || parser.extractXmlBool(casesensitive, u
"casesensitive"_sv);
1639 success = parser.checkIfExtracted(isExtracted);
1643 qWarning() << filename <<
"line" << xml.
lineNumber() <<
"missing attribute: regexpr";
1657 const auto pattern = regexp.
pattern();
1661 qWarning() << filename <<
"line" << line <<
"broken regex:" << pattern <<
"problem:" << regexp.
errorString() <<
"at offset"
1667 const int azOffset = std::max(pattern.
indexOf(u
"A-z"_sv), pattern.
indexOf(u
"a-Z"_sv));
1668 if (azOffset >= 0) {
1669 qWarning() << filename <<
"line" << line <<
"broken regex:" << pattern <<
"problem: [a-Z] or [A-z] at offset" << azOffset;
1678 bool checkContextAttribute(
const Definition &definition,
const Context &context)
const
1680 bool success =
true;
1682 if (!context.fallthroughContext.name.isEmpty()) {
1683 const bool mandatoryFallthroughAttribute = definition.kateVersion < KateVersion{5, 62};
1684 if (context.fallthrough == XmlBool::True && !mandatoryFallthroughAttribute) {
1685 qWarning() << definition.filename <<
"line" << context.line <<
"fallthrough attribute is unnecessary with kateversion >= 5.62 in context"
1688 }
else if (context.fallthrough != XmlBool::True && mandatoryFallthroughAttribute) {
1689 qWarning() << definition.filename <<
"line" << context.line
1690 <<
"fallthroughContext attribute without fallthrough=\"1\" attribute is only valid with kateversion >= 5.62 in context"
1696 if (context.stopEmptyLineContextSwitchLoop != XmlBool::Unspecified && definition.kateVersion < KateVersion{5, 103}) {
1697 qWarning() << definition.filename <<
"line" << context.line
1698 <<
"stopEmptyLineContextSwitchLoop attribute is only valid with kateversion >= 5.103 in context" << context.name;
1706 bool checkDelimiters(
const Definition &definition,
const Context::Rule &rule)
const
1708 if (rule.additionalDeliminator.isEmpty() && rule.weakDeliminator.isEmpty()) {
1712 bool success =
true;
1714 if (definition.kateVersion < KateVersion{5, 79}) {
1715 qWarning() << definition.filename <<
"line" << rule.line
1716 <<
"additionalDeliminator and weakDeliminator are only available since version \"5.79\". Please, increase kateversion.";
1720 for (
QChar c : rule.additionalDeliminator) {
1721 if (!definition.wordDelimiters.contains(c)) {
1726 for (
QChar c : rule.weakDeliminator) {
1727 if (definition.wordDelimiters.contains(c)) {
1732 qWarning() << rule.filename <<
"line" << rule.line <<
"unnecessary use of additionalDeliminator and/or weakDeliminator" << rule.string;
1737 bool checkKeyword(
const Definition &definition,
const Context::Rule &rule)
const
1739 if (rule.type == Context::Rule::Type::keyword) {
1740 auto it = definition.keywordsList.find(rule.string);
1741 if (it == definition.keywordsList.end()) {
1742 qWarning() << rule.filename <<
"line" << rule.line <<
"reference of non-existing keyword list:" << rule.string;
1751 bool checkLookAhead(
const Context::Rule &rule)
const
1753 if (rule.lookAhead == XmlBool::True && rule.context.stay) {
1754 qWarning() << rule.filename <<
"line" << rule.line <<
"infinite loop: lookAhead with context #stay";
1760 bool checkStringDetect(
const Context::Rule &rule)
const
1762 if (rule.type == Context::Rule::Type::StringDetect) {
1764 if (rule.dynamic == XmlBool::True) {
1766 if (!rule.string.contains(placeHolder)) {
1767 qWarning() << rule.filename <<
"line" << rule.line <<
"broken regex:" << rule.string <<
"problem: dynamic=true but no %\\d+ placeholder";
1776 bool checkWordDetect(
const Context::Rule &rule)
const
1778 if (rule.type == Context::Rule::Type::WordDetect) {
1779 if (!rule.string.isEmpty() && (rule.string.front().isSpace() || rule.string.back().isSpace())) {
1780 qWarning() << rule.filename <<
"line" << rule.line <<
"contains a space at the beginning or end of the string:" << rule.string;
1788 bool checkKeywordsList(
const Definition &definition)
const
1790 bool success =
true;
1792 bool includeNotSupport = (definition.kateVersion < KateVersion{5, 53});
1794 while (keywordsIt.hasNext()) {
1797 for (
const auto &include : keywordsIt.value().items.includes) {
1798 if (includeNotSupport) {
1799 qWarning() << definition.filename <<
"line" << include.line
1800 <<
"<include> is only available since version \"5.53\". Please, increase kateversion.";
1803 success = checkKeywordInclude(definition, include) && success;
1808 for (
const auto& keyword : keywordsIt.value().items.keywords) {
1809 for (
QChar c : keyword.content) {
1810 if (definition.wordDelimiters.contains(c)) {
1811 qWarning() << definition.filename <<
"line" << keyword.line <<
"keyword with delimiter:" << c <<
"in" << keyword.content;
1823 bool checkKeywordInclude(
const Definition &definition,
const Keywords::Items::Item &include)
const
1825 bool containsKeywordName =
true;
1826 int const idx = include.content.indexOf(u
"##"_sv);
1828 auto it = definition.keywordsList.find(include.content);
1829 containsKeywordName = (it != definition.keywordsList.end());
1831 auto defName = include.content.sliced(idx + 2);
1832 auto listName = include.content.sliced(0, idx);
1833 auto it = m_definitions.find(defName);
1834 if (it == m_definitions.end()) {
1835 qWarning() << definition.filename <<
"line" << include.line <<
"unknown definition in" << include.content;
1838 containsKeywordName = it->keywordsList.contains(listName);
1841 if (!containsKeywordName) {
1842 qWarning() << definition.filename <<
"line" << include.line <<
"unknown keyword name in" << include.content;
1845 return containsKeywordName;
1854 bool checkUreachableRules(
const QString &filename,
1855 const Context &context,
1858 if (context.isOnlyIncluded) {
1863 RuleAndInclude setRule(
const Context::Rule &rule,
const Context::Rule *includeRules =
nullptr)
1865 auto set = [&](RuleAndInclude &ruleAndInclude) {
1866 auto old = ruleAndInclude;
1867 ruleAndInclude = {&rule, includeRules};
1871 if (rule.firstNonSpace == XmlBool::True) {
1872 return set(firstNonSpace);
1873 }
else if (rule.column == 0) {
1874 return set(column0);
1875 }
else if (rule.column > 0) {
1876 return set(columnGreaterThan0[rule.column]);
1883 RuleAndInclude normal;
1884 RuleAndInclude column0;
1886 RuleAndInclude firstNonSpace;
1895 return m_asciiMap[c.
unicode()];
1897 auto it = m_utf8Map.find(c);
1898 return it == m_utf8Map.end() ? RuleAndInclude{
nullptr,
nullptr} : it.value();
1921 void append(
QChar c,
const Context::Rule &rule,
const Context::Rule *includeRule =
nullptr)
1924 m_asciiMap[c.
unicode()] = {&rule, includeRule};
1926 m_utf8Map[c] = {&rule, includeRule};
1931 void append(
QStringView s,
const Context::Rule &rule,
const Context::Rule *includeRule =
nullptr)
1934 append(c, rule, includeRule);
1939 RuleAndInclude m_asciiMap[127]{};
1943 struct Char4Tables {
1945 CharTable charsColumn0;
1947 CharTable charsFirstNonSpace;
1951 struct CharTableArray {
1954 CharTableArray(Char4Tables &tables,
const Context::Rule &rule)
1956 if (rule.firstNonSpace == XmlBool::True) {
1957 appendTable(tables.charsFirstNonSpace);
1960 if (rule.column == 0) {
1961 appendTable(tables.charsColumn0);
1962 }
else if (rule.column > 0) {
1963 appendTable(tables.charsColumnGreaterThan0[rule.column]);
1966 appendTable(tables.chars);
1970 void removeNonSpecialWhenSpecial()
1980 for (
int i = 0; i < m_size; ++i) {
1981 if (
auto ruleAndInclude = m_charTables[i]->
find(c)) {
1982 return ruleAndInclude;
1985 return RuleAndInclude{
nullptr,
nullptr};
1992 for (
int i = 0; i < m_size; ++i) {
1993 auto result = m_charTables[i]->find(s);
1994 if (result.
size()) {
1995 while (++i < m_size) {
2005 void append(
QChar c,
const Context::Rule &rule,
const Context::Rule *includeRule =
nullptr)
2007 for (
int i = 0; i < m_size; ++i) {
2008 m_charTables[i]->append(c, rule, includeRule);
2013 void append(
QStringView s,
const Context::Rule &rule,
const Context::Rule *includeRule =
nullptr)
2015 for (
int i = 0; i < m_size; ++i) {
2016 m_charTables[i]->append(s, rule, includeRule);
2021 void appendTable(CharTable &t)
2023 m_charTables[m_size] = &t;
2027 CharTable *m_charTables[3];
2031 struct ObservableRule {
2032 const Context::Rule *rule;
2033 const Context::Rule *includeRules;
2035 bool hasResolvedIncludeRules()
const
2037 return rule == includeRules;
2042 struct RuleIterator {
2044 : m_end(&endRule - rules.data())
2050 const Context::Rule *
next()
2053 if (m_includedRules) {
2055 if (m_i2 != m_includedRules->size()) {
2056 return (*m_includedRules)[m_i2];
2059 m_includedRules =
nullptr;
2063 while (m_i < m_end && m_rules[m_i].rule->type == Context::Rule::Type::IncludeRules) {
2064 if (!m_rules[m_i].includeRules && m_rules[m_i].rule->includedRules.size()) {
2066 m_includedRules = &m_rules[m_i].rule->includedRules;
2067 return (*m_includedRules)[m_i2];
2074 return m_rules[m_i - 1].rule;
2081 const Context::Rule *currentIncludeRules()
const
2083 return m_includedRules ? m_rules[m_i].rule : m_rules[m_i].includeRules;
2097 void append(
const Context::Rule &rule,
const Context::Rule *includedRule)
2099 auto array = extractDotRegexes(rule);
2101 *array[0] = {&rule, includedRule};
2104 *array[1] = {&rule, includedRule};
2109 RuleAndInclude
find(
const Context::Rule &rule)
2111 auto array = extractDotRegexes(rule);
2118 return RuleAndInclude{};
2122 using Array = std::array<RuleAndInclude *, 2>;
2124 Array extractDotRegexes(
const Context::Rule &rule)
2128 if (rule.firstNonSpace != XmlBool::True && rule.column == -1) {
2131 if (rule.firstNonSpace == XmlBool::True) {
2132 ret[0] = &dotRegexFirstNonSpace;
2135 if (rule.column == 0) {
2136 ret[1] = &dotRegexColumn0;
2137 }
else if (rule.column > 0) {
2138 ret[1] = &dotRegexColumnGreaterThan0[rule.column];
2145 RuleAndInclude dotRegex{};
2146 RuleAndInclude dotRegexColumn0{};
2148 RuleAndInclude dotRegexFirstNonSpace{};
2151 bool success =
true;
2154 Char4Tables detectChars;
2156 Char4Tables dynamicDetectChars;
2158 Char4Tables lineContinueChars;
2162 Rule4 hlCCharRule{};
2165 Rule4 hlCStringCharRule{};
2166 Rule4 detectIdentifierRule{};
2174 observedRules.
reserve(context.rules.size());
2175 for (
const Context::Rule &rule : context.rules) {
2176 const Context::Rule *includeRule =
nullptr;
2177 if (rule.type == Context::Rule::Type::IncludeRules) {
2178 auto *context = rule.context.context;
2179 if (context && context->isOnlyIncluded) {
2180 includeRule = &rule;
2184 observedRules.
push_back({&rule, includeRule});
2186 for (
const Context::Rule *rule2 : rule.includedRules) {
2187 observedRules.
push_back({rule2, includeRule});
2192 for (
auto &observedRule : observedRules) {
2193 const Context::Rule &rule = *observedRule.rule;
2194 bool isUnreachable =
false;
2198 auto updateUnreachable1 = [&](RuleAndInclude ruleAndInclude) {
2199 if (ruleAndInclude) {
2200 isUnreachable =
true;
2201 unreachableBy.
append(ruleAndInclude);
2207 if (!ruleAndIncludes.isEmpty()) {
2208 isUnreachable =
true;
2209 unreachableBy.
append(ruleAndIncludes);
2214 auto isCompatible = [&rule](Context::Rule
const &rule2) {
2215 return (rule2.firstNonSpace != XmlBool::True && rule2.column == -1) || (rule.column == rule2.column && rule.column != -1)
2216 || (rule.firstNonSpace == rule2.firstNonSpace && rule.firstNonSpace == XmlBool::True);
2219 updateUnreachable1(dotRegex.find(rule));
2221 switch (rule.type) {
2224 case Context::Rule::Type::AnyChar: {
2225 auto tables = CharTableArray(detectChars, rule);
2226 updateUnreachable2(tables.find(rule.string));
2227 tables.removeNonSpecialWhenSpecial();
2228 tables.append(rule.string, rule);
2234 case Context::Rule::Type::DetectChar: {
2235 auto &chars4 = (rule.dynamic != XmlBool::True) ? detectChars : dynamicDetectChars;
2236 auto tables = CharTableArray(chars4, rule);
2237 updateUnreachable1(tables.find(rule.char0));
2238 tables.removeNonSpecialWhenSpecial();
2239 tables.append(rule.char0, rule);
2245 case Context::Rule::Type::DetectSpaces: {
2246 auto tables = CharTableArray(detectChars, rule);
2247 updateUnreachable2(tables.find(u
" \t"_sv));
2248 tables.removeNonSpecialWhenSpecial();
2249 tables.append(u
' ', rule);
2250 tables.append(u
'\t', rule);
2255 case Context::Rule::Type::HlCChar:
2256 updateUnreachable1(CharTableArray(detectChars, rule).
find(u
'\''));
2257 updateUnreachable1(hlCCharRule.setRule(rule));
2261 case Context::Rule::Type::HlCHex:
2262 updateUnreachable1(CharTableArray(detectChars, rule).
find(u
'0'));
2263 updateUnreachable1(hlCHexRule.setRule(rule));
2267 case Context::Rule::Type::HlCOct:
2268 updateUnreachable1(CharTableArray(detectChars, rule).
find(u
'0'));
2269 updateUnreachable1(hlCOctRule.setRule(rule));
2273 case Context::Rule::Type::HlCStringChar:
2274 updateUnreachable1(CharTableArray(detectChars, rule).
find(u
'\\'));
2275 updateUnreachable1(hlCStringCharRule.setRule(rule));
2279 case Context::Rule::Type::Int:
2280 updateUnreachable2(CharTableArray(detectChars, rule).
find(u
"0123456789"_sv));
2281 updateUnreachable1(intRule.setRule(rule));
2285 case Context::Rule::Type::Float:
2286 updateUnreachable2(CharTableArray(detectChars, rule).
find(u
"0123456789."_sv));
2287 updateUnreachable1(floatRule.setRule(rule));
2289 updateUnreachable1(Rule4(intRule).setRule(rule));
2293 case Context::Rule::Type::DetectIdentifier:
2294 updateUnreachable1(detectIdentifierRule.setRule(rule));
2298 case Context::Rule::Type::LineContinue: {
2299 updateUnreachable1(CharTableArray(detectChars, rule).
find(rule.char0));
2301 auto tables = CharTableArray(lineContinueChars, rule);
2302 updateUnreachable1(tables.find(rule.char0));
2303 tables.removeNonSpecialWhenSpecial();
2304 tables.append(rule.char0, rule);
2309 case Context::Rule::Type::Detect2Chars:
2310 case Context::Rule::Type::RangeDetect:
2311 updateUnreachable1(CharTableArray(detectChars, rule).
find(rule.char0));
2312 if (!isUnreachable) {
2313 RuleIterator ruleIterator(observedRules, observedRule);
2314 while (
const auto *rulePtr = ruleIterator.next()) {
2315 if (isUnreachable) {
2318 const auto &rule2 = *rulePtr;
2319 if (rule2.type == rule.type && isCompatible(rule2) && rule.char0 == rule2.char0 && rule.char1 == rule2.char1) {
2320 updateUnreachable1({&rule2, ruleIterator.currentIncludeRules()});
2326 case Context::Rule::Type::RegExpr: {
2327 if (rule.isDotRegex) {
2328 dotRegex.append(rule,
nullptr);
2333 RuleIterator ruleIterator(observedRules, observedRule);
2334 while (
const auto *rulePtr = ruleIterator.next()) {
2335 if (isUnreachable) {
2338 const auto &rule2 = *rulePtr;
2339 if (rule2.type == Context::Rule::Type::RegExpr && isCompatible(rule2) && rule.insensitive == rule2.insensitive
2340 && rule.dynamic == rule2.dynamic && rule.sanitizedString.startsWith(rule2.sanitizedString)) {
2341 bool add = (rule.sanitizedString.startsWith(rule2.string) || rule.sanitizedString.size() < rule2.sanitizedString.size() + 2);
2345 auto c1 = rule.sanitizedString[rule2.sanitizedString.size()].unicode();
2346 auto c2 = rule.sanitizedString[rule2.sanitizedString.size() + 1].unicode();
2347 auto c3 = rule2.sanitizedString.back().unicode();
2348 if (c3 ==
'*' || c3 ==
'?' || c3 ==
'+') {
2350 }
else if (c1 ==
'*' || c1 ==
'?') {
2351 add = !((c2 ==
'?' || c2 ==
'+') || (rule.sanitizedString.size() >= rule2.sanitizedString.size() + 3));
2357 updateUnreachable1({&rule2, ruleIterator.currentIncludeRules()});
2365 case Context::Rule::Type::WordDetect:
2366 case Context::Rule::Type::StringDetect: {
2368 if (rule.type == Context::Rule::Type::StringDetect && rule.dynamic == XmlBool::True) {
2369 RuleIterator ruleIterator(observedRules, observedRule);
2370 while (
const auto *rulePtr = ruleIterator.next()) {
2371 if (isUnreachable) {
2375 const auto &rule2 = *rulePtr;
2376 if (rule2.type != Context::Rule::Type::StringDetect || rule2.dynamic != XmlBool::True || !isCompatible(rule2)) {
2380 const bool isSensitive = (rule2.insensitive == XmlBool::True);
2382 if ((isSensitive || rule.insensitive != XmlBool::True) && rule.string.startsWith(rule2.string, caseSensitivity)) {
2383 updateUnreachable1({&rule2, ruleIterator.currentIncludeRules()});
2392 if (rule.dynamic == XmlBool::True) {
2393 static const QRegularExpression dynamicPosition(QStringLiteral(R
"(^(?:[^%]*|%(?![1-9]))*)"));
2394 auto result = dynamicPosition.match(rule.string);
2395 s = s.
sliced(0, result.capturedLength());
2397 if (s.
size() + 2 <= rule.string.size()) {
2398 auto tables = CharTableArray(dynamicDetectChars, rule);
2399 updateUnreachable1(tables.find(s.
data()[s.
size() + 2]));
2406 if (rule.type == Context::Rule::Type::RegExpr) {
2407 static const QRegularExpression regularChars(QStringLiteral(R
"(^(?:[^.?*+^$[{(\\|]+|\\[-.?*+^$[\]{}()\\|]+|\[[^^\\]\])+)"));
2408 static const QRegularExpression sanitizeChars(QStringLiteral(R
"(\\([-.?*+^$[\]{}()\\|])|\[([^^\\])\])"));
2409 const qsizetype result = regularChars.match(rule.string).capturedLength();
2410 const qsizetype pos = qMin(result, s.
size());
2411 if (rule.string.indexOf(u
'|', pos) < pos) {
2412 sanitizedRegex = rule.string.
sliced(0, qMin(result, s.
size()));
2413 sanitizedRegex.
replace(sanitizeChars, QStringLiteral(
"\\1"));
2422 auto t = CharTableArray(detectChars, rule);
2423 if (rule.insensitive != XmlBool::True) {
2424 updateUnreachable1(t.find(s[0]));
2426 QChar c2[]{s[0].toLower(), s[0].toUpper()};
2431 if (rule.type == Context::Rule::Type::StringDetect && rule.string.size() == 1) {
2432 auto tables = CharTableArray(detectChars, rule);
2433 auto c = rule.string[0];
2434 if (rule.insensitive != XmlBool::True) {
2436 tables.removeNonSpecialWhenSpecial();
2437 tables.append(c, rule);
2440 tables.removeNonSpecialWhenSpecial();
2441 tables.append(c, rule);
2446 if (s.
size() > 0 && !isUnreachable) {
2448 RuleAndInclude detect2CharsInsensitives[]{{}, {}, {}, {}};
2450 RuleIterator ruleIterator(observedRules, observedRule);
2451 while (
const auto *rulePtr = ruleIterator.next()) {
2452 if (isUnreachable) {
2455 const auto &rule2 = *rulePtr;
2456 const bool isSensitive = (rule2.insensitive == XmlBool::True);
2459 switch (rule2.type) {
2461 case Context::Rule::Type::Detect2Chars:
2462 if (isCompatible(rule2) && s.
size() >= 2) {
2463 if (rule.insensitive != XmlBool::True) {
2464 if (rule2.char0 == s[0] && rule2.char1 == s[1]) {
2465 updateUnreachable1({&rule2, ruleIterator.currentIncludeRules()});
2470 auto set = [&](RuleAndInclude &x,
QChar c1,
QChar c2) {
2471 if (!x && rule2.char0 == c1 && rule2.char0 == c2) {
2472 x = {&rule2, ruleIterator.currentIncludeRules()};
2475 set(detect2CharsInsensitives[0], s[0].toLower(), s[1].toLower());
2476 set(detect2CharsInsensitives[1], s[0].toLower(), s[1].toUpper());
2477 set(detect2CharsInsensitives[2], s[0].toUpper(), s[1].toUpper());
2478 set(detect2CharsInsensitives[3], s[0].toUpper(), s[1].toLower());
2480 if (detect2CharsInsensitives[0] && detect2CharsInsensitives[1] && detect2CharsInsensitives[2]
2481 && detect2CharsInsensitives[3]) {
2482 isUnreachable =
true;
2483 unreachableBy.
append(detect2CharsInsensitives[0]);
2484 unreachableBy.
append(detect2CharsInsensitives[1]);
2485 unreachableBy.
append(detect2CharsInsensitives[2]);
2486 unreachableBy.
append(detect2CharsInsensitives[3]);
2493 case Context::Rule::Type::StringDetect:
2494 if (isCompatible(rule2) && rule2.dynamic != XmlBool::True && (isSensitive || rule.insensitive != XmlBool::True)
2495 && s.
startsWith(rule2.string, caseSensitivity)) {
2496 updateUnreachable1({&rule2, ruleIterator.currentIncludeRules()});
2501 case Context::Rule::Type::WordDetect:
2502 if (rule.type == Context::Rule::Type::WordDetect && isCompatible(rule2) && (isSensitive || rule.insensitive != XmlBool::True)
2503 && 0 == rule.string.compare(rule2.string, caseSensitivity)) {
2504 updateUnreachable1({&rule2, ruleIterator.currentIncludeRules()});
2517 case Context::Rule::Type::keyword: {
2518 RuleIterator ruleIterator(observedRules, observedRule);
2519 while (
const auto *rulePtr = ruleIterator.next()) {
2520 if (isUnreachable) {
2523 const auto &rule2 = *rulePtr;
2524 if (rule2.type == Context::Rule::Type::keyword && isCompatible(rule2) && rule.string == rule2.string) {
2525 updateUnreachable1({&rule2, ruleIterator.currentIncludeRules()});
2537 case Context::Rule::Type::IncludeRules:
2538 if (observedRule.includeRules && !observedRule.hasResolvedIncludeRules()) {
2542 if (
auto &ruleAndInclude = includeContexts[rule.context.context]) {
2543 updateUnreachable1(ruleAndInclude);
2545 ruleAndInclude.rule = &rule;
2548 for (
const auto *rulePtr : rule.includedIncludeRules) {
2549 includeContexts.
insert(rulePtr->context.context, RuleAndInclude{rulePtr, &rule});
2552 if (observedRule.includeRules) {
2556 for (
const auto *rulePtr : rule.includedRules) {
2557 const auto &rule2 = *rulePtr;
2558 switch (rule2.type) {
2559 case Context::Rule::Type::AnyChar: {
2560 auto tables = CharTableArray(detectChars, rule2);
2561 tables.removeNonSpecialWhenSpecial();
2562 tables.append(rule2.string, rule2, &rule);
2566 case Context::Rule::Type::DetectChar: {
2567 auto &chars4 = (rule2.dynamic != XmlBool::True) ? detectChars : dynamicDetectChars;
2568 auto tables = CharTableArray(chars4, rule2);
2569 tables.removeNonSpecialWhenSpecial();
2570 tables.append(rule2.char0, rule2, &rule);
2574 case Context::Rule::Type::DetectSpaces: {
2575 auto tables = CharTableArray(detectChars, rule2);
2576 tables.removeNonSpecialWhenSpecial();
2577 tables.append(u
' ', rule2, &rule);
2578 tables.append(u
'\t', rule2, &rule);
2582 case Context::Rule::Type::HlCChar:
2583 hlCCharRule.setRule(rule2, &rule);
2586 case Context::Rule::Type::HlCHex:
2587 hlCHexRule.setRule(rule2, &rule);
2590 case Context::Rule::Type::HlCOct:
2591 hlCOctRule.setRule(rule2, &rule);
2594 case Context::Rule::Type::HlCStringChar:
2595 hlCStringCharRule.setRule(rule2, &rule);
2598 case Context::Rule::Type::Int:
2599 intRule.setRule(rule2, &rule);
2602 case Context::Rule::Type::Float:
2603 floatRule.setRule(rule2, &rule);
2606 case Context::Rule::Type::LineContinue: {
2607 auto tables = CharTableArray(lineContinueChars, rule2);
2608 tables.removeNonSpecialWhenSpecial();
2609 tables.append(rule2.char0, rule2, &rule);
2613 case Context::Rule::Type::RegExpr:
2614 if (rule2.isDotRegex) {
2615 dotRegex.append(rule2, &rule);
2619 case Context::Rule::Type::StringDetect: {
2621 if (rule2.string.size() == 1 || (rule2.string.size() == 2 && rule2.dynamic == XmlBool::True)) {
2622 auto &chars4 = (rule2.dynamic != XmlBool::True) ? detectChars : dynamicDetectChars;
2623 auto tables = CharTableArray(chars4, rule2);
2624 tables.removeNonSpecialWhenSpecial();
2625 tables.append(rule2.string.back(), rule2, &rule);
2630 case Context::Rule::Type::WordDetect:
2631 case Context::Rule::Type::Detect2Chars:
2632 case Context::Rule::Type::IncludeRules:
2633 case Context::Rule::Type::DetectIdentifier:
2634 case Context::Rule::Type::keyword:
2635 case Context::Rule::Type::Unknown:
2636 case Context::Rule::Type::RangeDetect:
2642 case Context::Rule::Type::Unknown:
2646 if (observedRule.includeRules && !observedRule.hasResolvedIncludeRules()) {
2647 auto &unreachableIncludedRule = unreachableIncludedRules[&rule];
2648 if (isUnreachable && unreachableIncludedRule.alwaysUnreachable) {
2649 unreachableIncludedRule.unreachableBy.append(unreachableBy);
2651 unreachableIncludedRule.alwaysUnreachable =
false;
2653 }
else if (isUnreachable) {
2657 for (
auto &ruleAndInclude : std::as_const(unreachableBy)) {
2658 message += u
"line "_sv;
2659 if (ruleAndInclude.includeRules) {
2661 message += u
" [by '"_sv;
2662 message += ruleAndInclude.includeRules->context.name;
2663 message += u
"' line "_sv;
2665 if (ruleAndInclude.includeRules->filename != ruleAndInclude.rule->filename) {
2666 message += u
" ("_sv;
2667 message += ruleAndInclude.rule->filename;
2674 message += u
", "_sv;
2677 qWarning() << filename <<
"line" << rule.line <<
"unreachable rule by" << message;
2687 bool suggestRuleMerger(
const QString &filename,
const Context &context)
const
2689 bool success =
true;
2691 if (context.rules.isEmpty()) {
2695 auto it = context.rules.begin();
2696 const auto end = context.rules.end() - 1;
2698 for (; it <
end; ++it) {
2699 const auto &rule1 = *it;
2700 const auto &rule2 = it[1];
2702 auto isCommonCompatible = [&] {
2703 if (rule1.lookAhead != rule2.lookAhead) {
2707 if (rule1.lookAhead != XmlBool::True && rule1.attribute != rule2.attribute) {
2711 return rule1.beginRegion == rule2.beginRegion
2712 && rule1.endRegion == rule2.endRegion
2713 && rule1.firstNonSpace == rule2.firstNonSpace
2714 && rule1.context.context == rule2.context.context
2715 && rule1.context.popCount == rule2.context.popCount;
2719 switch (rule1.type) {
2721 case Context::Rule::Type::StringDetect:
2722 if (rule1.string.size() != 1 || rule1.dynamic == XmlBool::True) {
2727 case Context::Rule::Type::AnyChar:
2728 case Context::Rule::Type::DetectChar:
2729 if ((rule2.type == Context::Rule::Type::AnyChar || rule2.type == Context::Rule::Type::DetectChar
2730 || (rule2.type == Context::Rule::Type::StringDetect && rule2.dynamic != XmlBool::True && rule2.string.size() == 1))
2731 && isCommonCompatible() && rule1.column == rule2.column) {
2732 qWarning() << filename <<
"line" << rule2.line <<
"can be merged as AnyChar with the previous rule";
2738 case Context::Rule::Type::RegExpr:
2739 if (rule2.type == Context::Rule::Type::RegExpr && isCommonCompatible() && rule1.dynamic == rule2.dynamic
2740 && (rule1.column == rule2.column || (rule1.column <= 0 && rule2.column <= 0))) {
2741 qWarning() << filename <<
"line" << rule2.line <<
"can be merged with the previous rule";
2746 case Context::Rule::Type::DetectSpaces:
2747 case Context::Rule::Type::HlCChar:
2748 case Context::Rule::Type::HlCHex:
2749 case Context::Rule::Type::HlCOct:
2750 case Context::Rule::Type::HlCStringChar:
2751 case Context::Rule::Type::Int:
2752 case Context::Rule::Type::Float:
2753 case Context::Rule::Type::LineContinue:
2754 case Context::Rule::Type::WordDetect:
2755 case Context::Rule::Type::Detect2Chars:
2756 case Context::Rule::Type::IncludeRules:
2757 case Context::Rule::Type::DetectIdentifier:
2758 case Context::Rule::Type::keyword:
2759 case Context::Rule::Type::Unknown:
2760 case Context::Rule::Type::RangeDetect:
2776 void resolveContextName(Definition &definition, Context &context, ContextName &contextName,
int line)
2780 contextName.stay =
true;
2782 contextName.stay =
true;
2784 qWarning() << definition.filename <<
"line" << line <<
"invalid context in" << context.name;
2790 ++contextName.popCount;
2797 qWarning() << definition.filename <<
"line" << line <<
"'!' missing between '#pop' and context name" << context.name;
2805 auto it = definition.contexts.find(
name.toString());
2806 if (it != definition.contexts.end()) {
2807 contextName.context = &*it;
2811 auto it = m_definitions.find(defName.toString());
2812 if (it != m_definitions.end()) {
2813 auto listName =
name.
sliced(0, idx).toString();
2814 definition.referencedDefinitions.insert(&*it);
2815 auto ctxIt = it->contexts.find(listName.isEmpty() ? it->firstContextName : listName);
2816 if (ctxIt != it->contexts.end()) {
2817 contextName.context = &*ctxIt;
2820 qWarning() << definition.filename <<
"line" << line <<
"unknown definition in" << context.name;
2825 if (!contextName.context) {
2826 qWarning() << definition.filename <<
"line" << line <<
"unknown context" <<
name <<
"in" << context.name;
2835 Definition *m_currentDefinition =
nullptr;
2836 Keywords *m_currentKeywords =
nullptr;
2837 Context *m_currentContext =
nullptr;
2841 bool m_inKeywordItem =
false;
2843 bool m_success =
true;
2849 HlCompressor(
const QString &kateVersion)
2850 : m_kateVersion(kateVersion)
2852 m_hasElems.push_back(
true);
2855 const QString &compressedXML()
const
2873 closePreviousOpenTag(m_inContexts && !m_contexts.empty() ? m_contexts.back().data : m_data);
2874 m_hasElems.push_back(
false);
2876 const auto tagName = xml.
name();
2877 if (tagName == u
"contexts"_sv) {
2878 m_inContexts =
true;
2879 m_data += u
"<contexts"_sv;
2880 }
else if (m_inContexts) {
2881 Context &ctx = (m_contexts.empty() || tagName == u
"context"_sv) ? m_contexts.emplace_back() : m_contexts.back();
2883 const bool isDetect2Chars = tagName == u
"Detect2Chars"_sv;
2884 out += u
'<' % (isDetect2Chars ? u
"StringDetect"_sv : tagName);
2887 sortAttributes(attrs);
2888 for (
const auto &attr : attrs) {
2889 const auto attrName = attr.
name();
2890 auto value = attr.
value();
2892 if (isDetect2Chars && (attrName == u
"char"_sv || attrName == u
"char1"_sv)) {
2893 if (attrName == u
"char"_sv) {
2894 const auto ch0 = value;
2895 const auto ch1 = attrs.value(u
"char1"_sv);
2896 QChar chars[]{ch0.isEmpty() ? u
' ' : ch0[0], ch1.isEmpty() ? u
' ' : ch1[0]};
2897 writeXmlAttribute(out, u
"String"_sv,
QStringView(chars, 2), tagName);
2899 }
else if (attrName == u
"context"_sv || attrName == u
"lineEndContext"_sv || attrName == u
"fallthroughContext"_sv
2900 || attrName == u
"lineEmptyContext"_sv) {
2902 if (value != u
"#stay"_sv) {
2903 writeXmlAttribute(out, attrName, value, tagName);
2908 bool hasPop =
false;
2909 while (value.startsWith(u
"#pop"_sv)) {
2911 value = value.sliced(4);
2913 if (hasPop && !value.isEmpty()) {
2914 value = value.sliced(1);
2916 if (!value.isEmpty() && -1 == value.indexOf(u
"##"_sv)) {
2917 m_contextRefs[value.toString()]++;
2920 }
else if (tagName == u
"LineContinue"_sv && attrName == u
"char"_sv && value == u
"\\") {
2923 if (attrName == u
"name"_sv) {
2924 ctx.name = value.toString();
2926 writeXmlAttribute(out, attrName, value, tagName);
2930 m_data += u
'<' % tagName;
2932 for (
const auto &attr : attrs) {
2935 writeXmlAttribute(m_data, name, value, tagName);
2943 if (m_inContexts && !m_contexts.empty() && name == u
"contexts"_sv) {
2944 m_inContexts =
false;
2946 std::sort(m_contexts.begin() + 1, m_contexts.end(), [&](
auto &ctx1,
auto &ctx2) {
2947 auto i1 = m_contextRefs.value(ctx1.name);
2948 auto i2 = m_contextRefs.value(ctx2.name);
2953 return ctx1.name < ctx2.name;
2955 for (
const auto &ctx : m_contexts) {
2960 QString &out = m_inContexts && !m_contexts.empty() ? m_contexts.
back().data : m_data;
2961 if (m_hasElems.back()) {
2962 out += u
"</"_sv %
name % u
'>';
2966 m_hasElems.pop_back();
2973 closePreviousOpenTag(m_data);
2974 writeXmlText(m_data, xml.
text());
2983 void closePreviousOpenTag(
QString &out)
2985 if (!m_hasElems.back()) {
2986 m_hasElems.back() =
true;
2996 for (
const QChar &c : text) {
2999 }
else if (c == u
'&') {
3001 }
else if (escapeDQ && c == u
'"') {
3003 }
else if (c == u
'\t') {
3020 enum class DefaultBool {
3037 {u
"fallthrough"_sv, DefaultBool::Ignored},
3038 {u
"dynamic"_sv, DefaultBool::DynamicAttr},
3039 {u
"hidden"_sv, DefaultBool::False},
3040 {u
"indentationsensitive"_sv, DefaultBool::False},
3041 {u
"noIndentationBasedFolding"_sv, DefaultBool::False},
3042 {u
"lookAhead"_sv, DefaultBool::False},
3043 {u
"firstNonSpace"_sv, DefaultBool::False},
3044 {u
"insensitive"_sv, DefaultBool::FalseOrKeywordTag},
3045 {u
"minimal"_sv, DefaultBool::False},
3046 {u
"includeAttrib"_sv, DefaultBool::False},
3047 {u
"italic"_sv, DefaultBool::None},
3048 {u
"bold"_sv, DefaultBool::None},
3049 {u
"underline"_sv, DefaultBool::None},
3050 {u
"strikeOut"_sv, DefaultBool::None},
3051 {u
"spellChecking"_sv, DefaultBool::True},
3052 {u
"casesensitive"_sv, DefaultBool::TrueOrKeywordsTag},
3053 {u
"ignored"_sv, DefaultBool::Ignored},
3056 auto it = booleanAttrs.
find(attrName);
3058 if (it != booleanAttrs.end()) {
3059 bool b = KSyntaxHighlighting::Xml::attrToBool(value);
3060 bool ignoreAttr =
false;
3062 case DefaultBool::Ignored:
3065 case DefaultBool::TrueOrKeywordsTag:
3066 ignoreAttr = (tagName == u
"keywords"_sv) ?
false : b;
3068 case DefaultBool::True:
3071 case DefaultBool::FalseOrKeywordTag:
3072 ignoreAttr = (tagName == u
"keyword"_sv) ?
false : !b;
3074 case DefaultBool::DynamicAttr:
3075 ignoreAttr = (tagName == u
"context"_sv) || !b;
3077 case DefaultBool::False:
3080 case DefaultBool::None:
3085 out += u
' ' % attrName % u
"=\""_sv % (b ? u
'1' : u
'0') % u
'"';
3088 const bool hasDQ = value.
contains(u
'"');
3090 if (!hasDQ || value.
contains(u
'\'')) {
3091 out += u
' ' % attrName % u
"=\""_sv;
3092 writeXmlText(out, value, hasDQ);
3096 out += u
' ' % attrName % u
"='"_sv;
3097 writeXmlText(out, value);
3110 {u
"attribute"_sv, 5},
3116 {u
"noIndentationBasedFolding"_sv, 11},
3117 {u
"lineEndContext"_sv, 9},
3118 {u
"lineEmptyContext"_sv, 8},
3119 {u
"fallthroughContext"_sv, 7},
3122 {u
"lookAhead"_sv, 100},
3123 {u
"firstNonSpace"_sv, 99},
3124 {u
"dynamic"_sv, 98},
3125 {u
"minimal"_sv, 97},
3126 {u
"includeAttrib"_sv, 96},
3127 {u
"insensitive"_sv, 95},
3129 {u
"beginRegion"_sv, 40},
3130 {u
"endRegion"_sv, 41},
3131 {u
"weakDeliminator"_sv, 31},
3132 {u
"additionalDeliminator"_sv, 30},
3133 {u
"context"_sv, 20},
3138 {u
"strikeOut"_sv, 100},
3139 {u
"underline"_sv, 99},
3142 {u
"spellChecking"_sv, 96},
3143 {u
"defStyleNum"_sv, 95},
3145 {u
"backgroundColor"_sv, 93},
3146 {u
"selBackgroundColor"_sv, 92},
3147 {u
"selColor"_sv, 91},
3149 std::sort(attrs.
begin(), attrs.
end(), [](
auto &attr1,
auto &attr2) {
3150 auto i1 = priorityAttrs.value(attr1.name());
3151 auto i2 = priorityAttrs.value(attr2.name());
3155 return attr1.name() < attr2.name();
3163 QString m_data = u
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE language>"_s;
3164 std::vector<Context> m_contexts;
3168 bool m_inContexts =
false;
3171void printFileError(
const QFile &file)
3183 QFile file(fileName);
3185 printFileError(file);
3191 while (!xml.
atEnd()) {
3201 printXmlError(fileName, xml);
3219 if (extensionParts.
isEmpty()) {
3224 for (
const auto &extension : extensionParts) {
3225 for (
const auto c : extension) {
3232 if (c == u
'.' || c == u
'-' || c == u
'_' || c == u
'+') {
3237 if (c == u
'?' || c == u
'*') {
3241 qWarning() <<
"invalid character" << c <<
"seen in extensions wildcard";
3250struct CompressedFile {
3257int main(
int argc,
char *argv[])
3263 if (app.arguments().size() < 4) {
3269 XMLPlatformUtils::Initialize();
3270 auto cleanup = qScopeGuard(XMLPlatformUtils::Terminate);
3275 XMLGrammarPoolImpl xsd(XMLPlatformUtils::fgMemoryManager);
3278 CustomXMLValidator parser(&xsd);
3281 const auto xsdFile = app.arguments().at(2);
3282 if (!parser.loadGrammar((
const char16_t *)xsdFile.utf16(), Grammar::SchemaGrammarType,
true) || parser.eh.failed()) {
3283 qWarning(
"Failed to parse XSD %s: %s", qPrintable(xsdFile), qPrintable(parser.messages));
3291 const QString hlFilenamesListing = app.arguments().value(3);
3292 if (hlFilenamesListing.
isEmpty()) {
3296 QStringList hlFilenames = readListing(hlFilenamesListing);
3298 qWarning(
"Failed to read %s", qPrintable(hlFilenamesListing));
3303 const QStringList textAttributes =
QStringList() << QStringLiteral(
"name") << QStringLiteral(
"alternativeNames") << QStringLiteral(
"section")
3304 << QStringLiteral(
"mimetype") << QStringLiteral(
"extensions") << QStringLiteral(
"style")
3305 << QStringLiteral(
"author") << QStringLiteral(
"license") << QStringLiteral(
"indenter");
3308 HlFilesChecker filesChecker;
3311 std::vector<CompressedFile> compressedFiles;
3312 for (
const QString &hlFilename : std::as_const(hlFilenames)) {
3313 QFile hlFile(hlFilename);
3315 printFileError(hlFile);
3322 CustomXMLValidator parser(&xsd);
3325 parser.parse((
const char16_t *)hlFile.fileName().utf16());
3328 if (parser.eh.failed()) {
3329 qWarning(
"Failed to validate XML %s: %s", qPrintable(hlFile.fileName()), qPrintable(parser.messages));
3352 for (
const QString &attribute : std::as_const(textAttributes)) {
3357 if (!checkExtensions(hl[QStringLiteral(
"extensions")].
toString())) {
3358 qWarning() << hlFilename <<
"'extensions' wildcards invalid:" << hl[QStringLiteral(
"extensions")].toString();
3370 hl[QStringLiteral(
"nameUtf8")] = hl[QStringLiteral(
"name")].toString().toUtf8();
3371 hl[QStringLiteral(
"sectionUtf8")] = hl[QStringLiteral(
"section")].toString().toUtf8();
3377 const QString hlName = hl[QStringLiteral(
"name")].toString();
3378 const QString hlAlternativeNames = hl[QStringLiteral(
"alternativeNames")].toString();
3380 filesChecker.setDefinition(kateversion, hlFilename, hlName, hlAlternativeNames.
split(u
';',
Qt::SkipEmptyParts));
3385 HlCompressor compressor((filesChecker.currentVersion() < KateVersion{5, 62}) ? u
"5.62"_s : kateversion.
toString());
3386 compressor.processElement(xml);
3389 while (!xml.
atEnd()) {
3391 filesChecker.processElement(xml);
3392 compressor.processElement(xml);
3397 printXmlError(hlFilename, xml);
3400 compressedFiles.emplace_back(CompressedFile{
3402 compressor.compressedXML(),
3406 filesChecker.resolveContexts();
3408 if (!filesChecker.check()) {
3418 HlFilesChecker filesChecker2;
3419 const QString compressedDir = app.arguments().
at(4) + u
"/"_sv;
3420 for (
const auto &compressedFile : std::as_const(compressedFiles)) {
3421 const auto outFileName = compressedDir + compressedFile.fileName;
3422 auto utf8Data = compressedFile.xmlData.
toUtf8();
3426 CustomXMLValidator parser(&xsd);
3428 auto utf8Filename = outFileName.toUtf8();
3429 utf8Filename.append(
'\0');
3431 MemBufInputSource membuf(
reinterpret_cast<const XMLByte *
>(utf8Data.constData()), utf8Data.size(), utf8Filename.data());
3434 if (parser.eh.failed()) {
3435 qWarning(
"Failed to validate XML %s: %s", qPrintable(outFileName), qPrintable(parser.messages));
3444 while (!xml.
atEnd()) {
3445 if (xml.
readNext() == QXmlStreamReader::TokenType::StartElement && xml.
name() == u
"language"_sv) {
3447 const auto version = attrs.value(u
"kateversion"_sv);
3448 const QString hlName = attrs.value(u
"name"_sv).toString();
3449 const QString hlAlternativeNames = attrs.value(u
"alternativeNames"_sv).toString();
3450 filesChecker2.setDefinition(version, outFileName, hlName, hlAlternativeNames.
split(u
';',
Qt::SkipEmptyParts));
3452 filesChecker2.processElement(xml);
3456 printXmlError(outFileName, xml);
3461 QFile outFile(outFileName);
3465 outFile.write(utf8Data);
3468 filesChecker2.resolveContexts();
3471 if (!filesChecker2.check()) {
3476 QFile outFile(app.arguments().at(1));
AKONADI_MIME_EXPORT const char Ignored[]
Type type(const QSqlDatabase &db)
char * toString(const EngineQuery &query)
KDB_EXPORT KDbVersionInfo version()
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
KIOCORE_EXPORT void add(const QString &fileClass, const QString &directory)
QString name(StandardAction id)
const QList< QKeySequence > & next()
const QList< QKeySequence > & find()
const QList< QKeySequence > & end()
const QList< QKeySequence > & replace()
KTEXTEDITOR_EXPORT size_t qHash(KTextEditor::Cursor cursor, size_t seed=0) noexcept
bool operator<(const PosRange< Trait > &l, const PosRange< Trait > &r)
bool operator==(const StyleDelim &l, const StyleDelim &r)
QCborValue fromVariant(const QVariant &variant)
bool isDigit(char32_t ucs4)
bool isLetter(char32_t ucs4)
char32_t toLower(char32_t ucs4)
char32_t toUpper(char32_t ucs4)
virtual QString fileName() const const override
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
QString fileName() const const
iterator find(const Key &key)
QString errorString() const const
void append(QList< T > &&value)
bool isEmpty() const const
void push_back(parameter_type value)
void reserve(qsizetype size)
qsizetype size() const const
iterator find(const Key &key)
iterator insert(const Key &key, const T &value)
QString errorString() const const
bool isValid() const const
QString pattern() const const
qsizetype patternErrorOffset() const const
bool contains(const QSet< T > &other) const const
iterator erase(const_iterator pos)
iterator insert(const T &value)
qsizetype size() const const
const QChar at(qsizetype position) const const
QString fromUtf16(const char16_t *unicode, qsizetype size)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
void reserve(qsizetype size)
qsizetype size() const const
QString sliced(qsizetype pos) const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toUtf8() const const
bool contains(QChar c, Qt::CaseSensitivity cs) const const
const_pointer data() const const
QChar first() const const
qsizetype indexOf(QChar c, qsizetype from, Qt::CaseSensitivity cs) const const
bool isNull() const const
qsizetype size() const const
QStringView sliced(qsizetype pos) const const
QList< QStringView > split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar ch) const const
int toInt(bool *ok, int base) const const
QString toString() const const
QTextStream & endl(QTextStream &stream)
QStringView name() const const
QStringView value() const const
QStringView value(QAnyStringView namespaceUri, QAnyStringView name) const const
QXmlStreamAttributes attributes() const const
qint64 characterOffset() const const
QString errorString() const const
bool hasError() const const
bool isCharacters() const const
bool isWhitespace() const const
qint64 lineNumber() const const
QStringView name() const const
bool readNextStartElement()
QStringView text() const const
TokenType tokenType() const const