// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
// Qt-Security score:significant

#include "qqmljsliteralbindingcheck_p.h"

#include <private/qqmljsimportvisitor_p.h>
#include <private/qqmljstyperesolver_p.h>
#include <private/qqmljsmetatypes_p.h>
#include <private/qqmlsa_p.h>
#include <private/qqmlsasourcelocation_p.h>
#include <private/qqmlstringconverters_p.h>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

// This makes no sense, but we want to warn about things QQmlPropertyResolver complains about.
static bool canConvertForLiteralBinding(QQmlJSTypeResolver *resolver,
                                        const QQmlSA::Element &fromElement,
                                        const QQmlSA::Element &toElement)
{
    Q_ASSERT(resolver);
    auto from = QQmlJSScope::scope(fromElement);
    auto to = QQmlJSScope::scope(toElement);
    if (from == to)
        return true;

    if (!resolver->canConvertFromTo(from, to))
        return false;

    const bool fromIsString = from == resolver->stringType();

    if (to == resolver->stringType()
            || to == resolver->stringListType()
            || to == resolver->byteArrayType()
            || to == resolver->urlType()) {
        return fromIsString;
    }

    if (resolver->isNumeric(to))
        return resolver->isNumeric(from);

    if (to == resolver->boolType())
        return from == resolver->boolType();

    return true;
}

QQmlJSLiteralBindingCheck::QQmlJSLiteralBindingCheck(QQmlSA::PassManager *passManager)
    : LiteralBindingCheckBase(passManager),
      m_resolver(QQmlSA::PassManagerPrivate::resolver(*passManager))
{
}

QQmlJSStructuredTypeError QQmlJSLiteralBindingCheck::check(const QString &typeName,
                                                           const QString &value) const
{
    return QQmlJSValueTypeFromStringCheck::hasError(typeName, value);
}

static QString literalPrettyTypeName(QQmlSA::BindingType type)
{
    switch (type) {
    case QQmlSA::BindingType::BoolLiteral:
        return u"bool"_s;
    case QQmlSA::BindingType::NumberLiteral:
        return u"double"_s;
    case QQmlSA::BindingType::StringLiteral:
        return u"string"_s;
    case QQmlSA::BindingType::RegExpLiteral:
        return u"regexp"_s;
    case QQmlSA::BindingType::Null:
        return u"null"_s;
    default:
        return QString();
    }
    Q_UNREACHABLE_RETURN(QString());
}

QQmlSA::Property LiteralBindingCheckBase::getProperty(const QString &propertyName,
                                                      const QQmlSA::Binding &binding,
                                                      const QQmlSA::Element &bindingScope) const
{
    if (!QQmlSA::Binding::isLiteralBinding(binding.bindingType()))
        return {};

    const QString unqualifiedPropertyName = [&propertyName]() -> QString {
        if (auto idx = propertyName.lastIndexOf(u'.'); idx != -1 && idx != propertyName.size() - 1)
            return propertyName.sliced(idx + 1);
        return propertyName;
    }();

    return bindingScope.property(unqualifiedPropertyName);
}

void LiteralBindingCheckBase::warnOnCheckedBinding(
        const QQmlSA::Binding &binding, const QQmlSA::Element &propertyType)
{
    auto construction = check(propertyType.internalId(), binding.stringValue());
    if (!construction.isValid())
        return;

    const QString warningMessage =
            u"Construction from string is deprecated. Use structured value type "
            u"construction instead for type \"%1\""_s.arg(propertyType.internalId());

    if (construction.code.isEmpty()) {
        emitWarning(warningMessage, qmlIncompatibleType, binding.sourceLocation());
        return;
    }

    const QQmlSA::FixSuggestion suggestion(
            u"Replace string by structured value construction"_s,
            binding.sourceLocation(), construction.code);
    emitWarning(warningMessage, qmlIncompatibleType, binding.sourceLocation(), suggestion);
}

void QQmlJSLiteralBindingCheck::onBinding(const QQmlSA::Element &element,
                                          const QString &propertyName,
                                          const QQmlSA::Binding &binding,
                                          const QQmlSA::Element &bindingScope,
                                          const QQmlSA::Element &value)
{
    Q_UNUSED(value);

    const auto property = getProperty(propertyName, binding, bindingScope);
    if (!property.isValid())
        return;

    // If the property is defined in the same scope where it is set,
    // we are in fact allowed to set it, even if it's not writable.
    if (property.isReadonly() && !element.hasOwnProperty(propertyName)) {
        emitWarning(u"Cannot assign to read-only property %1"_s.arg(propertyName),
                    qmlReadOnlyProperty, binding.sourceLocation());
        return;
    }

    if (const auto propertyType = property.type())
        warnOnCheckedBinding(binding, propertyType);

    if (!canConvertForLiteralBinding(m_resolver, resolveLiteralType(binding), property.type())) {
        emitWarning(u"Cannot assign literal of type %1 to %2"_s.arg(
                            literalPrettyTypeName(binding.bindingType()),
                            QQmlJSScope::prettyName(property.typeName())),
                    qmlIncompatibleType, binding.sourceLocation());
        return;
    }
}

QT_END_NAMESPACE
