qmllint: Uncruftify FixSuggestion

In this form we can expose it in QQmlSA.

Pick-to: 6.5
Task-number: QTBUG-110834
Change-Id: Ieb0cf31b6e86379c0d80f89bc6c63b129f269798
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
Ulf Hermann 2023-02-06 12:14:38 +01:00
parent e3831511de
commit 403f4117e2
10 changed files with 227 additions and 197 deletions

View File

@ -814,7 +814,7 @@ void QQmlJSImportVisitor::checkRequiredProperties()
? getScopeName(prevRequiredScope, QQmlJSScope::QMLScope) ? getScopeName(prevRequiredScope, QQmlJSScope::QMLScope)
: u"here"_s; : u"here"_s;
std::optional<FixSuggestion> suggestion; std::optional<QQmlJSFixSuggestion> suggestion;
QString message = QString message =
QStringLiteral( QStringLiteral(
@ -824,15 +824,15 @@ void QQmlJSImportVisitor::checkRequiredProperties()
if (requiredScope != scope) { if (requiredScope != scope) {
if (!prevRequiredScope.isNull()) { if (!prevRequiredScope.isNull()) {
auto sourceScope = prevRequiredScope->baseType(); auto sourceScope = prevRequiredScope->baseType();
suggestion = FixSuggestion { suggestion = QQmlJSFixSuggestion {
{ { u"%1:%2:%3: Property marked as required in %4"_s "%1:%2:%3: Property marked as required in %4"_L1
.arg(sourceScope->filePath()) .arg(sourceScope->filePath())
.arg(sourceScope->sourceLocation().startLine) .arg(sourceScope->sourceLocation().startLine)
.arg(sourceScope->sourceLocation().startColumn) .arg(sourceScope->sourceLocation().startColumn)
.arg(requiredScopeName), .arg(requiredScopeName),
sourceScope->sourceLocation(), QString(), sourceScope->sourceLocation()
sourceScope->filePath() } }
}; };
suggestion->setFilename(sourceScope->filePath());
} else { } else {
message += QStringLiteral(" (marked as required by %1)") message += QStringLiteral(" (marked as required by %1)")
.arg(requiredScopeName); .arg(requiredScopeName);
@ -863,7 +863,7 @@ void QQmlJSImportVisitor::processPropertyBindings()
continue; continue;
// TODO: Can this be in a better suited category? // TODO: Can this be in a better suited category?
std::optional<FixSuggestion> fixSuggestion; std::optional<QQmlJSFixSuggestion> fixSuggestion;
for (QQmlJSScope::ConstPtr baseScope = scope; !baseScope.isNull(); for (QQmlJSScope::ConstPtr baseScope = scope; !baseScope.isNull();
baseScope = baseScope->baseType()) { baseScope = baseScope->baseType()) {
@ -945,7 +945,7 @@ void QQmlJSImportVisitor::checkSignal(
} }
if (!signalMethod.has_value()) { // haven't found anything if (!signalMethod.has_value()) { // haven't found anything
std::optional<FixSuggestion> fix; std::optional<QQmlJSFixSuggestion> fix;
// There is a small chance of suggesting this fix for things that are not actually // There is a small chance of suggesting this fix for things that are not actually
// QtQml/Connections elements, but rather some other thing that is also called // QtQml/Connections elements, but rather some other thing that is also called
@ -957,15 +957,13 @@ void QQmlJSImportVisitor::checkSignal(
const qsizetype newLength = m_logger->code().indexOf(u'\n', location.end()) const qsizetype newLength = m_logger->code().indexOf(u'\n', location.end())
- location.offset; - location.offset;
fix = FixSuggestion { { FixSuggestion::Fix { fix = QQmlJSFixSuggestion {
QStringLiteral("Implicitly defining %1 as signal handler in " "Implicitly defining %1 as signal handler in Connections is deprecated. "
"Connections is deprecated. Create a function " "Create a function instead"_L1.arg(handlerName),
"instead")
.arg(handlerName),
QQmlJS::SourceLocation(location.offset, newLength, location.startLine, QQmlJS::SourceLocation(location.offset, newLength, location.startLine,
location.startColumn), location.startColumn),
QStringLiteral("function %1(%2) { ... }") "function %1(%2) { ... }"_L1.arg(handlerName, handlerParameters.join(u", "))
.arg(handlerName, handlerParameters.join(u", ")) } } }; };
} }
m_logger->log(QStringLiteral("no matching signal found for handler \"%1\"") m_logger->log(QStringLiteral("no matching signal found for handler \"%1\"")
@ -1366,9 +1364,12 @@ bool QQmlJSImportVisitor::visit(QQmlJS::AST::StringLiteral *sl)
templateString += c; templateString += c;
} }
const FixSuggestion suggestion = { { { u"Use a template literal instead"_s, QQmlJSFixSuggestion suggestion = {
sl->literalToken, u"`" % templateString % u"`", "Use a template literal instead"_L1,
QString(), false } } }; sl->literalToken,
u"`" % templateString % u"`"
};
suggestion.setAutoApplicable();
m_logger->log(QStringLiteral("String contains unescaped line terminator which is " m_logger->log(QStringLiteral("String contains unescaped line terminator which is "
"deprecated."), "deprecated."),
qmlMultilineStrings, sl->literalToken, true, true, suggestion); qmlMultilineStrings, sl->literalToken, true, true, suggestion);

View File

@ -323,7 +323,7 @@ void QQmlJSLinter::parseComments(QQmlJSLogger *logger,
} }
static void addJsonWarning(QJsonArray &warnings, const QQmlJS::DiagnosticMessage &message, static void addJsonWarning(QJsonArray &warnings, const QQmlJS::DiagnosticMessage &message,
QAnyStringView id, const std::optional<FixSuggestion> &suggestion = {}) QAnyStringView id, const std::optional<QQmlJSFixSuggestion> &suggestion = {})
{ {
QJsonObject jsonMessage; QJsonObject jsonMessage;
@ -362,19 +362,35 @@ static void addJsonWarning(QJsonArray &warnings, const QQmlJS::DiagnosticMessage
jsonMessage[u"message"_s] = message.message; jsonMessage[u"message"_s] = message.message;
QJsonArray suggestions; QJsonArray suggestions;
const auto convertLocation = [](const QQmlJS::SourceLocation &source, QJsonObject *target) {
target->insert("line"_L1, int(source.startLine));
target->insert("column"_L1, int(source.startColumn));
target->insert("charOffset"_L1, int(source.offset));
target->insert("length"_L1, int(source.length));
};
if (suggestion.has_value()) { if (suggestion.has_value()) {
for (const auto &fix : suggestion->fixes) { QJsonObject jsonFix {
QJsonObject jsonFix; { "message"_L1, suggestion->fixDescription() },
jsonFix[u"message"] = fix.message; { "replacement"_L1, suggestion->replacement() },
jsonFix[u"line"_s] = static_cast<int>(fix.cutLocation.startLine); { "isHint"_L1, !suggestion->isAutoApplicable() },
jsonFix[u"column"_s] = static_cast<int>(fix.cutLocation.startColumn); };
jsonFix[u"charOffset"_s] = static_cast<int>(fix.cutLocation.offset); convertLocation(suggestion->location(), &jsonFix);
jsonFix[u"length"_s] = static_cast<int>(fix.cutLocation.length); const QString filename = suggestion->filename();
jsonFix[u"replacement"_s] = fix.replacementString; if (!filename.isEmpty())
jsonFix[u"isHint"] = fix.isHint; jsonFix.insert("fileName"_L1, filename);
if (!fix.fileName.isEmpty()) suggestions << jsonFix;
jsonFix[u"fileName"] = fix.fileName;
suggestions << jsonFix; const QString hint = suggestion->hint();
if (!hint.isEmpty()) {
// We need to keep compatibility with the JSON format.
// Therefore the overly verbose encoding of the hint.
QJsonObject jsonHint {
{ "message"_L1, hint },
{ "replacement"_L1, QString() },
{ "isHint"_L1, true }
};
convertLocation(QQmlJS::SourceLocation(), &jsonHint);
suggestions << jsonHint;
} }
} }
jsonMessage[u"suggestions"] = suggestions; jsonMessage[u"suggestions"] = suggestions;
@ -778,7 +794,7 @@ QQmlJSLinter::FixResult QQmlJSLinter::applyFixes(QString *fixedCode, bool silent
QString code = m_fileContents; QString code = m_fileContents;
QList<FixSuggestion::Fix> fixesToApply; QList<QQmlJSFixSuggestion> fixesToApply;
QFileInfo info(m_logger->fileName()); QFileInfo info(m_logger->fileName());
const QString currentFileAbsolutePath = info.absoluteFilePath(); const QString currentFileAbsolutePath = info.absoluteFilePath();
@ -792,34 +808,30 @@ QQmlJSLinter::FixResult QQmlJSLinter::applyFixes(QString *fixedCode, bool silent
for (const auto &messages : { m_logger->infos(), m_logger->warnings(), m_logger->errors() }) for (const auto &messages : { m_logger->infos(), m_logger->warnings(), m_logger->errors() })
for (const Message &msg : messages) { for (const Message &msg : messages) {
if (!msg.fixSuggestion.has_value()) if (!msg.fixSuggestion.has_value() || !msg.fixSuggestion->isAutoApplicable())
continue; continue;
for (const auto &fix : msg.fixSuggestion->fixes) { // Ignore fix suggestions for other files
if (fix.isHint) const QString filename = msg.fixSuggestion->filename();
continue; if (!filename.isEmpty()
&& QFileInfo(filename).absoluteFilePath() != currentFileAbsolutePath) {
// Ignore fix suggestions for other files continue;
if (!fix.fileName.isEmpty()
&& QFileInfo(fix.fileName).absoluteFilePath() != currentFileAbsolutePath) {
continue;
}
fixesToApply << fix;
} }
fixesToApply << msg.fixSuggestion.value();
} }
if (fixesToApply.isEmpty()) if (fixesToApply.isEmpty())
return NothingToFix; return NothingToFix;
std::sort(fixesToApply.begin(), fixesToApply.end(), std::sort(fixesToApply.begin(), fixesToApply.end(),
[](FixSuggestion::Fix &a, FixSuggestion::Fix &b) { [](const QQmlJSFixSuggestion &a, const QQmlJSFixSuggestion &b) {
return a.cutLocation.offset < b.cutLocation.offset; return a.location().offset < b.location().offset;
}); });
for (auto it = fixesToApply.begin(); it + 1 != fixesToApply.end(); it++) { for (auto it = fixesToApply.begin(); it + 1 != fixesToApply.end(); it++) {
QQmlJS::SourceLocation srcLocA = it->cutLocation; const QQmlJS::SourceLocation srcLocA = it->location();
QQmlJS::SourceLocation srcLocB = (it + 1)->cutLocation; const QQmlJS::SourceLocation srcLocB = (it + 1)->location();
if (srcLocA.offset + srcLocA.length > srcLocB.offset) { if (srcLocA.offset + srcLocA.length > srcLocB.offset) {
if (!silent) if (!silent)
qWarning() << "Fixes for two warnings are overlapping, aborting. Please file a bug " qWarning() << "Fixes for two warnings are overlapping, aborting. Please file a bug "
@ -831,12 +843,14 @@ QQmlJSLinter::FixResult QQmlJSLinter::applyFixes(QString *fixedCode, bool silent
int offsetChange = 0; int offsetChange = 0;
for (const auto &fix : fixesToApply) { for (const auto &fix : fixesToApply) {
qsizetype cutLocation = fix.cutLocation.offset + offsetChange; const QQmlJS::SourceLocation fixLocation = fix.location();
QString before = code.left(cutLocation); qsizetype cutLocation = fixLocation.offset + offsetChange;
QString after = code.mid(cutLocation + fix.cutLocation.length); const QString before = code.left(cutLocation);
const QString after = code.mid(cutLocation + fixLocation.length);
code = before + fix.replacementString + after; const QString replacement = fix.replacement();
offsetChange += fix.replacementString.size() - fix.cutLocation.length; code = before + replacement + after;
offsetChange += replacement.size() - fixLocation.length;
} }
QQmlJS::Engine engine; QQmlJS::Engine engine;

View File

@ -219,7 +219,7 @@ static bool isMsgTypeLess(QtMsgType a, QtMsgType b)
void QQmlJSLogger::log(const QString &message, LoggerWarningId id, void QQmlJSLogger::log(const QString &message, LoggerWarningId id,
const QQmlJS::SourceLocation &srcLocation, QtMsgType type, bool showContext, const QQmlJS::SourceLocation &srcLocation, QtMsgType type, bool showContext,
bool showFileName, const std::optional<FixSuggestion> &suggestion, bool showFileName, const std::optional<QQmlJSFixSuggestion> &suggestion,
const QString overrideFileName) const QString overrideFileName)
{ {
Q_ASSERT(m_categoryLevels.contains(id.name().toString())); Q_ASSERT(m_categoryLevels.contains(id.name().toString()));
@ -324,59 +324,58 @@ void QQmlJSLogger::printContext(const QString &overrideFileName,
+ QString::fromLatin1("^").repeated(locationLength) + QLatin1Char('\n')); + QString::fromLatin1("^").repeated(locationLength) + QLatin1Char('\n'));
} }
void QQmlJSLogger::printFix(const FixSuggestion &fix) void QQmlJSLogger::printFix(const QQmlJSFixSuggestion &fixItem)
{ {
const QString currentFileAbsPath = QFileInfo(m_fileName).absolutePath(); const QString currentFileAbsPath = QFileInfo(m_fileName).absolutePath();
QString code = m_code; QString code = m_code;
QString currentFile; QString currentFile;
for (const auto &fixItem : fix.fixes) { m_output.writePrefixedMessage(fixItem.fixDescription(), QtInfoMsg);
m_output.writePrefixedMessage(fixItem.message, QtInfoMsg);
if (!fixItem.cutLocation.isValid()) if (!fixItem.location().isValid())
continue; return;
if (fixItem.fileName == currentFile) { const QString filename = fixItem.filename();
// Nothing to do in this case, we've already read the code if (filename == currentFile) {
} else if (fixItem.fileName.isEmpty() || fixItem.fileName == currentFileAbsPath) { // Nothing to do in this case, we've already read the code
code = m_code; } else if (filename.isEmpty() || filename == currentFileAbsPath) {
} else { code = m_code;
QFile file(fixItem.fileName); } else {
const bool success = file.open(QFile::ReadOnly); QFile file(filename);
Q_ASSERT(success); const bool success = file.open(QFile::ReadOnly);
code = QString::fromUtf8(file.readAll()); Q_ASSERT(success);
currentFile = fixItem.fileName; code = QString::fromUtf8(file.readAll());
} currentFile = filename;
IssueLocationWithContext issueLocationWithContext { code, fixItem.cutLocation };
if (const QStringView beforeText = issueLocationWithContext.beforeText();
!beforeText.isEmpty()) {
m_output.write(beforeText);
}
// The replacement string can be empty if we're only pointing something out to the user
QStringView replacementString = fixItem.replacementString.isEmpty()
? issueLocationWithContext.issueText()
: fixItem.replacementString;
// But if there's nothing to change it has to be a hint
if (fixItem.replacementString.isEmpty())
Q_ASSERT(fixItem.isHint);
m_output.write(replacementString, QtDebugMsg);
m_output.write(issueLocationWithContext.afterText().toString() + u'\n');
int tabCount = issueLocationWithContext.beforeText().count(u'\t');
// Do not draw location indicator for multiline replacement strings
if (replacementString.contains(u'\n'))
continue;
m_output.write(u" "_s.repeated(
issueLocationWithContext.beforeText().size() - tabCount)
+ u"\t"_s.repeated(tabCount)
+ u"^"_s.repeated(fixItem.replacementString.size()) + u'\n');
} }
IssueLocationWithContext issueLocationWithContext { code, fixItem.location() };
if (const QStringView beforeText = issueLocationWithContext.beforeText();
!beforeText.isEmpty()) {
m_output.write(beforeText);
}
// The replacement string can be empty if we're only pointing something out to the user
const QString replacement = fixItem.replacement();
QStringView replacementString = replacement.isEmpty()
? issueLocationWithContext.issueText()
: replacement;
// But if there's nothing to change it cannot be auto-applied
Q_ASSERT(!replacement.isEmpty() || !fixItem.isAutoApplicable());
m_output.write(replacementString, QtDebugMsg);
m_output.write(issueLocationWithContext.afterText().toString() + u'\n');
int tabCount = issueLocationWithContext.beforeText().count(u'\t');
// Do not draw location indicator for multiline replacement strings
if (replacementString.contains(u'\n'))
return;
m_output.write(u" "_s.repeated(
issueLocationWithContext.beforeText().size() - tabCount)
+ u"\t"_s.repeated(tabCount)
+ u"^"_s.repeated(replacement.size()) + u'\n');
} }
QT_END_NAMESPACE QT_END_NAMESPACE

View File

@ -70,19 +70,37 @@ private:
QStringView m_afterText; QStringView m_afterText;
}; };
struct Q_QMLCOMPILER_PRIVATE_EXPORT FixSuggestion class Q_QMLCOMPILER_PRIVATE_EXPORT QQmlJSFixSuggestion
{ {
struct Fix public:
{ QQmlJSFixSuggestion() = default;
QString message; QQmlJSFixSuggestion(const QString &fixDescription, const QQmlJS::SourceLocation &location,
QQmlJS::SourceLocation cutLocation = QQmlJS::SourceLocation(); const QString &replacement = QString())
QString replacementString = QString(); : m_location(location)
QString fileName = QString(); , m_fixDescription(fixDescription)
// A Fix is a hint if it can not be automatically applied to fix an issue or only points out , m_replacement(replacement)
// its origin {}
bool isHint = true;
}; QString fixDescription() const { return m_fixDescription; }
QList<Fix> fixes; QQmlJS::SourceLocation location() const { return m_location; }
QString replacement() const { return m_replacement; }
void setFilename(const QString &filename) { m_filename = filename; }
QString filename() const { return m_filename; }
void setHint(const QString &hint) { m_hint = hint; }
QString hint() const { return m_hint; }
void setAutoApplicable(bool autoApply = true) { m_autoApplicable = autoApply; }
bool isAutoApplicable() const { return m_autoApplicable; }
private:
QQmlJS::SourceLocation m_location;
QString m_fixDescription;
QString m_replacement;
QString m_filename;
QString m_hint;
bool m_autoApplicable = false;
}; };
class Q_QMLCOMPILER_PRIVATE_EXPORT LoggerWarningId class Q_QMLCOMPILER_PRIVATE_EXPORT LoggerWarningId
@ -137,7 +155,7 @@ struct Message : public QQmlJS::DiagnosticMessage
// This doesn't need to be an owning-reference since the string is expected to outlive any // This doesn't need to be an owning-reference since the string is expected to outlive any
// Message object by virtue of coming from a LoggerWarningId. // Message object by virtue of coming from a LoggerWarningId.
QAnyStringView id; QAnyStringView id;
std::optional<FixSuggestion> fixSuggestion; std::optional<QQmlJSFixSuggestion> fixSuggestion;
}; };
class Q_QMLCOMPILER_PRIVATE_EXPORT QQmlJSLogger class Q_QMLCOMPILER_PRIVATE_EXPORT QQmlJSLogger
@ -253,7 +271,7 @@ public:
*/ */
void log(const QString &message, LoggerWarningId id, const QQmlJS::SourceLocation &srcLocation, void log(const QString &message, LoggerWarningId id, const QQmlJS::SourceLocation &srcLocation,
bool showContext = true, bool showFileName = true, bool showContext = true, bool showFileName = true,
const std::optional<FixSuggestion> &suggestion = {}, const std::optional<QQmlJSFixSuggestion> &suggestion = {},
const QString overrideFileName = QString()) const QString overrideFileName = QString())
{ {
log(message, id, srcLocation, m_categoryLevels[id.name().toString()], showContext, log(message, id, srcLocation, m_categoryLevels[id.name().toString()], showContext,
@ -281,11 +299,11 @@ private:
QMap<QString, Category> m_categories; QMap<QString, Category> m_categories;
void printContext(const QString &overrideFileName, const QQmlJS::SourceLocation &location); void printContext(const QString &overrideFileName, const QQmlJS::SourceLocation &location);
void printFix(const FixSuggestion &fix); void printFix(const QQmlJSFixSuggestion &fix);
void log(const QString &message, LoggerWarningId id, const QQmlJS::SourceLocation &srcLocation, void log(const QString &message, LoggerWarningId id, const QQmlJS::SourceLocation &srcLocation,
QtMsgType type, bool showContext, bool showFileName, QtMsgType type, bool showContext, bool showFileName,
const std::optional<FixSuggestion> &suggestion, const QString overrideFileName); const std::optional<QQmlJSFixSuggestion> &suggestion, const QString overrideFileName);
QString m_fileName; QString m_fileName;
QString m_code; QString m_code;

View File

@ -292,7 +292,7 @@ void QQmlJSTypePropagator::handleUnqualifiedAccess(const QString &name, bool isM
return; return;
} }
std::optional<FixSuggestion> suggestion; std::optional<QQmlJSFixSuggestion> suggestion;
auto childScopes = m_function->qmlScope->childScopes(); auto childScopes = m_function->qmlScope->childScopes();
for (qsizetype i = 0; i < m_function->qmlScope->childScopes().size(); i++) { for (qsizetype i = 0; i < m_function->qmlScope->childScopes().size(); i++) {
@ -307,9 +307,6 @@ void QQmlJSTypePropagator::handleUnqualifiedAccess(const QString &name, bool isM
const auto jsId = scope->childScopes().first()->findJSIdentifier(name); const auto jsId = scope->childScopes().first()->findJSIdentifier(name);
if (jsId.has_value() && jsId->kind == QQmlJSScope::JavaScriptIdentifier::Injected) { if (jsId.has_value() && jsId->kind == QQmlJSScope::JavaScriptIdentifier::Injected) {
suggestion = FixSuggestion {};
const QQmlJSScope::JavaScriptIdentifier id = jsId.value(); const QQmlJSScope::JavaScriptIdentifier id = jsId.value();
QQmlJS::SourceLocation fixLocation = id.location; QQmlJS::SourceLocation fixLocation = id.location;
@ -328,15 +325,15 @@ void QQmlJSTypePropagator::handleUnqualifiedAccess(const QString &name, bool isM
fixString += handler.isMultiline ? u") "_s : u") => "_s; fixString += handler.isMultiline ? u") "_s : u") => "_s;
suggestion->fixes << FixSuggestion::Fix { suggestion = QQmlJSFixSuggestion {
name name + u" is accessible in this scope because you are handling a signal"
+ QString::fromLatin1(" is accessible in this scope because " " at %1:%2. Use a function instead.\n"_s
"you are handling a signal at %1:%2. Use a " .arg(id.location.startLine)
"function instead.\n") .arg(id.location.startColumn),
.arg(id.location.startLine) fixLocation,
.arg(id.location.startColumn), fixString
fixLocation, fixString, QString(), false
}; };
suggestion->setAutoApplicable();
} }
break; break;
} }
@ -354,11 +351,10 @@ void QQmlJSTypePropagator::handleUnqualifiedAccess(const QString &name, bool isM
if (!it->hasObject()) if (!it->hasObject())
continue; continue;
if (it->objectType() == m_function->qmlScope) { if (it->objectType() == m_function->qmlScope) {
suggestion = FixSuggestion {}; suggestion = QQmlJSFixSuggestion {
name + " is implicitly injected into this delegate."
suggestion->fixes << FixSuggestion::Fix { " Add a required property instead."_L1,
name + u" is implicitly injected into this delegate. Add a required property instead."_s, m_function->qmlScope->sourceLocation()
m_function->qmlScope->sourceLocation(), QString(), QString(), true
}; };
}; };
@ -373,36 +369,34 @@ void QQmlJSTypePropagator::handleUnqualifiedAccess(const QString &name, bool isM
if (scope->hasProperty(name)) { if (scope->hasProperty(name)) {
const QString id = m_function->addressableScopes.id(scope); const QString id = m_function->addressableScopes.id(scope);
suggestion = FixSuggestion {};
QQmlJS::SourceLocation fixLocation = location; QQmlJS::SourceLocation fixLocation = location;
fixLocation.length = 0; fixLocation.length = 0;
suggestion->fixes << FixSuggestion::Fix { suggestion = QQmlJSFixSuggestion {
name + QLatin1String(" is a member of a parent element\n") name + " is a member of a parent element.\n You can qualify the access "
+ QLatin1String(" You can qualify the access with its id " "with its id to avoid this warning:\n"_L1,
"to avoid this warning:\n"), fixLocation,
fixLocation, (id.isEmpty() ? u"<id>."_s : (id + u'.')), QString(), id.isEmpty() (id.isEmpty() ? u"<id>."_s : (id + u'.'))
}; };
if (id.isEmpty()) { if (id.isEmpty())
suggestion->fixes << FixSuggestion::Fix { suggestion->setHint("You first have to give the element an id"_L1);
u"You first have to give the element an id"_s, QQmlJS::SourceLocation {}, {} else
}; suggestion->setAutoApplicable();
}
} }
} }
} }
if (!suggestion.has_value() && !m_function->addressableScopes.componentsAreBound() if (!suggestion.has_value() && !m_function->addressableScopes.componentsAreBound()
&& m_function->addressableScopes.existsAnywhereInDocument(name)) { && m_function->addressableScopes.existsAnywhereInDocument(name)) {
FixSuggestion::Fix bindComponents;
const QLatin1String replacement = "pragma ComponentBehavior: Bound"_L1; const QLatin1String replacement = "pragma ComponentBehavior: Bound"_L1;
bindComponents.replacementString = replacement + '\n'_L1; QQmlJSFixSuggestion bindComponents {
bindComponents.message = "Set \"%1\" in order to use IDs " "Set \"%1\" in order to use IDs from outer components in nested components."_L1
"from outer components in nested components."_L1.arg(replacement); .arg(replacement),
bindComponents.cutLocation = QQmlJS::SourceLocation(0, 0, 1, 1); QQmlJS::SourceLocation(0, 0, 1, 1),
bindComponents.isHint = false; replacement + '\n'_L1
suggestion = FixSuggestion {{ bindComponents }}; };
bindComponents.setAutoApplicable();
suggestion = bindComponents;
} }
if (!suggestion.has_value()) { if (!suggestion.has_value()) {
@ -758,25 +752,19 @@ void QQmlJSTypePropagator::propagatePropertyLookup(const QString &propertyName)
const QString id = m_function->addressableScopes.id(scope); const QString id = m_function->addressableScopes.id(scope);
FixSuggestion suggestion;
QQmlJS::SourceLocation fixLocation = getCurrentSourceLocation(); QQmlJS::SourceLocation fixLocation = getCurrentSourceLocation();
fixLocation.length = 0; fixLocation.length = 0;
suggestion.fixes << FixSuggestion::Fix { u"Reference it by id instead:"_s, QQmlJSFixSuggestion suggestion {
fixLocation, "Reference it by id instead:"_L1,
id.isEmpty() ? u"<id>."_s : (id + u'.'), fixLocation,
QString(), id.isEmpty() }; id.isEmpty() ? u"<id>."_s : (id + u'.')
};
fixLocation = scope->sourceLocation(); if (id.isEmpty())
fixLocation.length = 0; suggestion.setHint("You first have to give the element an id"_L1);
else
if (id.isEmpty()) { suggestion.setAutoApplicable();
suggestion.fixes
<< FixSuggestion::Fix { u"You first have to give the element an id"_s,
QQmlJS::SourceLocation {},
{} };
}
m_logger->log( m_logger->log(
u"Using attached type %1 already initialized in a parent scope."_s.arg( u"Using attached type %1 already initialized in a parent scope."_s.arg(
@ -832,7 +820,7 @@ void QQmlJSTypePropagator::propagatePropertyLookup(const QString &propertyName)
if (propertyResolution(baseType, propertyName) != PropertyMissing) if (propertyResolution(baseType, propertyName) != PropertyMissing)
return; return;
std::optional<FixSuggestion> fixSuggestion; std::optional<QQmlJSFixSuggestion> fixSuggestion;
if (auto suggestion = QQmlJSUtils::didYouMean(propertyName, baseType->properties().keys(), if (auto suggestion = QQmlJSUtils::didYouMean(propertyName, baseType->properties().keys(),
getCurrentSourceLocation()); getCurrentSourceLocation());
@ -1107,7 +1095,7 @@ void QQmlJSTypePropagator::generate_CallProperty(int nameIndex, int base, int ar
if (isRestricted(propertyName)) if (isRestricted(propertyName))
return; return;
std::optional<FixSuggestion> fixSuggestion; std::optional<QQmlJSFixSuggestion> fixSuggestion;
const auto baseType = m_typeResolver->containedType(callBase); const auto baseType = m_typeResolver->containedType(callBase);

View File

@ -97,7 +97,7 @@ QQmlJSUtils::ResolvedAlias QQmlJSUtils::resolveAlias(const QQmlJSScopesById &idS
property, owner, visitor); property, owner, visitor);
} }
std::optional<FixSuggestion> QQmlJSUtils::didYouMean(const QString &userInput, std::optional<QQmlJSFixSuggestion> QQmlJSUtils::didYouMean(const QString &userInput,
QStringList candidates, QStringList candidates,
QQmlJS::SourceLocation location) QQmlJS::SourceLocation location)
{ {
@ -144,10 +144,12 @@ std::optional<FixSuggestion> QQmlJSUtils::didYouMean(const QString &userInput,
} }
if (shortestDistance if (shortestDistance
< std::min(std::max(userInput.size() / 2, qsizetype(3)), userInput.size())) { < std::min(std::max(userInput.size() / 2, qsizetype(3)), userInput.size())) {
return FixSuggestion { { FixSuggestion::Fix { return QQmlJSFixSuggestion {
u"Did you mean \"%1\"?"_s.arg(shortestDistanceWord), location, u"Did you mean \"%1\"?"_s.arg(shortestDistanceWord),
shortestDistanceWord } } }; location,
shortestDistanceWord
};
} else { } else {
return {}; return {};
} }

View File

@ -359,7 +359,7 @@ struct Q_QMLCOMPILER_PRIVATE_EXPORT QQmlJSUtils
act(begin->scope, begin->extensionSpecifier); act(begin->scope, begin->extensionSpecifier);
} }
static std::optional<FixSuggestion> didYouMean(const QString &userInput, static std::optional<QQmlJSFixSuggestion> didYouMean(const QString &userInput,
QStringList candidates, QStringList candidates,
QQmlJS::SourceLocation location); QQmlJS::SourceLocation location);

View File

@ -31,10 +31,17 @@ GenericPass::GenericPass(PassManager *manager)
d->manager = manager; d->manager = manager;
} }
void GenericPass::emitWarning(QAnyStringView message, LoggerWarningId id, void GenericPass::emitWarning(QAnyStringView diagnostic, LoggerWarningId id,
QQmlJS::SourceLocation srcLocation) QQmlJS::SourceLocation srcLocation)
{ {
d->manager->m_visitor->logger()->log(message.toString(), id, srcLocation); d->manager->m_visitor->logger()->log(diagnostic.toString(), id, srcLocation);
}
void GenericPass::emitWarning(QAnyStringView diagnostic, LoggerWarningId id,
QQmlJS::SourceLocation srcLocation, const FixSuggestion &fix)
{
d->manager->m_visitor->logger()->log(diagnostic.toString(), id, srcLocation, true, true, fix);
} }
Element GenericPass::resolveType(QAnyStringView moduleName, QAnyStringView typeName) Element GenericPass::resolveType(QAnyStringView moduleName, QAnyStringView typeName)

View File

@ -35,6 +35,7 @@ namespace QQmlSA {
// ### FIXME: Replace with a proper PIMPL'd type // ### FIXME: Replace with a proper PIMPL'd type
using Element = QQmlJSScope::ConstPtr; using Element = QQmlJSScope::ConstPtr;
using FixSuggestion = QQmlJSFixSuggestion;
class GenericPassPrivate; class GenericPassPrivate;
class PassManager; class PassManager;
@ -46,8 +47,10 @@ public:
GenericPass(PassManager *manager); GenericPass(PassManager *manager);
virtual ~GenericPass(); virtual ~GenericPass();
void emitWarning(QAnyStringView message, LoggerWarningId id, void emitWarning(QAnyStringView diagnostic, LoggerWarningId id,
QQmlJS::SourceLocation srcLocation = QQmlJS::SourceLocation()); QQmlJS::SourceLocation srcLocation = QQmlJS::SourceLocation());
void emitWarning(QAnyStringView diagnostic, LoggerWarningId id,
QQmlJS::SourceLocation srcLocation, const FixSuggestion &fix);
Element resolveType(QAnyStringView moduleName, QAnyStringView typeName); // #### TODO: revisions Element resolveType(QAnyStringView moduleName, QAnyStringView typeName); // #### TODO: revisions
Element resolveLiteralType(const QQmlJSMetaPropertyBinding &binding); Element resolveLiteralType(const QQmlJSMetaPropertyBinding &binding);
Element resolveId(QAnyStringView id, const Element &context); Element resolveId(QAnyStringView id, const Element &context);

View File

@ -198,28 +198,26 @@ void QmlLintSuggestions::diagnose(const QByteArray &url)
// We need to interject the information about where the fix suggestions end // We need to interject the information about where the fix suggestions end
// here since we don't have access to the textDocument to calculate it later. // here since we don't have access to the textDocument to calculate it later.
QJsonArray fixedSuggestions; QJsonArray fixedSuggestions;
for (const FixSuggestion::Fix &fix : suggestion->fixes) { const QQmlJS::SourceLocation cut = suggestion->location();
QQmlJS::SourceLocation cut = fix.cutLocation;
int line = cut.isValid() ? cut.startLine - 1 : 0; const int line = cut.isValid() ? cut.startLine - 1 : 0;
int column = cut.isValid() ? cut.startColumn - 1 : 0; const int column = cut.isValid() ? cut.startColumn - 1 : 0;
QJsonObject object; QJsonObject object;
object[u"lspBeginLine"] = line; object.insert("lspBeginLine"_L1, line);
object[u"lspBeginCharacter"] = column; object.insert("lspBeginCharacter"_L1, column);
Position end = { line, column }; Position end = { line, column };
addLength(end, srcLoc.isValid() ? cut.offset : 0, addLength(end, srcLoc.isValid() ? cut.offset : 0,
srcLoc.isValid() ? cut.length : 0); srcLoc.isValid() ? cut.length : 0);
object[u"lspEndLine"] = end.line; object.insert("lspEndLine"_L1, end.line);
object[u"lspEndCharacter"] = end.character; object.insert("lspEndCharacter"_L1, end.character);
object[u"message"] = fix.message; object.insert("message"_L1, suggestion->fixDescription());
object[u"replacement"] = fix.replacementString; object.insert("replacement"_L1, suggestion->replacement());
fixedSuggestions << object; fixedSuggestions << object;
}
QJsonObject data; QJsonObject data;
data[u"suggestions"] = fixedSuggestions; data[u"suggestions"] = fixedSuggestions;