// Copyright (C) 2024 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 reason:default

#include "qdslintplugin.h"

#include <QtCore/qlist.h>
#include <QtCore/qvarlengtharray.h>
#include <QtCore/qhash.h>
#include <QtCore/qset.h>
#include <QtCore/qspan.h>

#include <QtQmlCompiler/private/qqmljsscope_p.h>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;
using namespace QQmlSA;

// note: is a warning, but is prefixed Err to share the name with its QtC codemodel counterpart.
constexpr LoggerWarningId ErrFunctionsNotSupportedInQmlUi{
    "QtDesignStudio.FunctionsNotSupportedInQmlUi"
};
constexpr LoggerWarningId WarnReferenceToParentItemNotSupportedByVisualDesigner{
    "QtDesignStudio.ReferenceToParentItemNotSupportedByVisualDesigner"
};
constexpr LoggerWarningId WarnImperativeCodeNotEditableInVisualDesigner{
    "QtDesignStudio.ImperativeCodeNotEditableInVisualDesigner"
};
constexpr LoggerWarningId ErrUnsupportedTypeInQmlUi{ "QtDesignStudio.UnsupportedTypeInQmlUi" };
constexpr LoggerWarningId ErrInvalidIdeInVisualDesigner{
    "QtDesignStudio.InvalidIdeInVisualDesigner"
};
constexpr LoggerWarningId ErrUnsupportedRootTypeInQmlUi{
    "QtDesignStudio.UnsupportedRootTypeInQmlUi"
};
constexpr LoggerWarningId qmlTranslationFunctionMismatch{
    "QtDesignStudio.TranslationFunctionMismatch"
};

class FunctionCallValidator : public PropertyPass
{
public:
    FunctionCallValidator(PassManager *manager)
    : PropertyPass(manager)
    , m_connectionsType(resolveType("QtQuick", "Connections")) {}

    void onCall(const Element &element, const QString &propertyName, const Element &readScope,
                SourceLocation location) override;
private:
    Element m_connectionsType;
};

class QdsBindingValidator : public PropertyPass
{
public:
    QdsBindingValidator(PassManager *manager, const Element &)
        : PropertyPass(manager), m_statesType(resolveType("QtQuick", "State"))
    {
    }

    void onRead(const QQmlSA::Element &element, const QString &propertyName,
                const QQmlSA::Element &readScope, QQmlSA::SourceLocation location) override;

    void onWrite(const QQmlSA::Element &element, const QString &propertyName,
                 const QQmlSA::Element &value, const QQmlSA::Element &writeScope,
                 QQmlSA::SourceLocation location) override;

private:
    Element m_statesType;
};

class QdsElementValidator : public ElementPass
{
public:
    QdsElementValidator(PassManager *passManager);
    void run(const Element &element) override;

private:
    void complainAboutFunctions(const Element &element);
    static constexpr std::array s_unsupportedElementNames = {
        std::make_pair("QtQuick.Controls"_L1, "ApplicationWindow"_L1),
        std::make_pair("QtQuick.Controls"_L1, "Drawer"_L1),
        std::make_pair("QtQml.Models"_L1, "Package"_L1),
        std::make_pair("QtQuick"_L1, "ShaderEffect"_L1),
    };
    using UnsupportedName = decltype(s_unsupportedElementNames)::value_type;
    std::array<Element, s_unsupportedElementNames.size()> m_unsupportedElements;

    static constexpr std::array s_unsupportedRootNames = {
        std::make_pair("QtQml.Models"_L1, "ListModel"_L1),
        std::make_pair("QtQml.Models"_L1, "Package"_L1),
        std::make_pair("QtQml"_L1, "Timer"_L1),
    };
    std::array<Element, s_unsupportedRootNames.size()> m_unsupportedRootElements;
    std::array<Element, 2> m_supportFunctions;
    Element m_qtObject;
};

class QQmlJSTranslationFunctionMismatchCheck : public QQmlSA::PropertyPass
{
public:
    using QQmlSA::PropertyPass::PropertyPass;

    void onCall(const QQmlSA::Element &element, const QString &propertyName,
                const QQmlSA::Element &readScope, QQmlSA::SourceLocation location) override;

private:
    enum TranslationType : quint8 { None, Normal, IdBased };
    TranslationType m_lastTranslationFunction = None;
};

void QdsBindingValidator::onRead(const QQmlSA::Element &element, const QString &propertyName,
                                 const QQmlSA::Element &readScope, QQmlSA::SourceLocation location)
{
    Q_UNUSED(readScope);

    if (element.isFileRootComponent() && propertyName == u"parent") {
        emitWarning("Referencing the parent of the root item is not supported in a UI file (.ui.qml)",
                    WarnReferenceToParentItemNotSupportedByVisualDesigner, location);
    }
}

void QdsBindingValidator::onWrite(const QQmlSA::Element &, const QString &propertyName,
                                  const QQmlSA::Element &, const QQmlSA::Element &,
                                  QQmlSA::SourceLocation location)
{
    static constexpr std::array forbiddenAssignments = { "baseline"_L1,
                                                         "baselineOffset"_L1,
                                                         "bottomMargin"_L1,
                                                         "centerIn"_L1,
                                                         "color"_L1,
                                                         "fill"_L1,
                                                         "height"_L1,
                                                         "horizontalCenter"_L1,
                                                         "horizontalCenterOffset"_L1,
                                                         "left"_L1,
                                                         "leftMargin"_L1,
                                                         "margins"_L1,
                                                         "mirrored"_L1,
                                                         "opacity"_L1,
                                                         "right"_L1,
                                                         "rightMargin"_L1,
                                                         "rotation"_L1,
                                                         "scale"_L1,
                                                         "topMargin"_L1,
                                                         "verticalCenter"_L1,
                                                         "verticalCenterOffset"_L1,
                                                         "width"_L1,
                                                         "x"_L1,
                                                         "y"_L1,
                                                         "z"_L1 };
    Q_ASSERT(std::is_sorted(forbiddenAssignments.cbegin(), forbiddenAssignments.cend()));
    if (std::find(forbiddenAssignments.cbegin(), forbiddenAssignments.cend(), propertyName)
        != forbiddenAssignments.cend()) {
        emitWarning("Imperative JavaScript assignments can break the visual tooling in Qt Design "
                    "Studio.",
                    WarnImperativeCodeNotEditableInVisualDesigner, location);
    }
}

void QQmlJSTranslationFunctionMismatchCheck::onCall(const QQmlSA::Element &element,
                                                    const QString &propertyName,
                                                    const QQmlSA::Element &readScope,
                                                    QQmlSA::SourceLocation location)
{
    Q_UNUSED(readScope);

    const QQmlSA::Element globalJSObject = resolveBuiltinType(u"GlobalObject");
    if (element != globalJSObject)
        return;

    constexpr std::array translationFunctions = {
        "qsTranslate"_L1,
        "QT_TRANSLATE_NOOP"_L1,
        "qsTr"_L1,
        "QT_TR_NOOP"_L1,
    };

    constexpr std::array idTranslationFunctions = {
        "qsTrId"_L1,
        "QT_TRID_NOOP"_L1,
    };

    const bool isTranslation =
            std::find(translationFunctions.cbegin(), translationFunctions.cend(), propertyName)
            != translationFunctions.cend();
    const bool isIdTranslation =
            std::find(idTranslationFunctions.cbegin(), idTranslationFunctions.cend(), propertyName)
            != idTranslationFunctions.cend();

    if (!isTranslation && !isIdTranslation)
        return;

    const TranslationType current = isTranslation ? Normal : IdBased;

    if (m_lastTranslationFunction == None) {
        m_lastTranslationFunction = current;
        return;
    }

    if (m_lastTranslationFunction != current) {
        emitWarning("Do not mix translation functions", qmlTranslationFunctionMismatch, location);
    }
}

void QmlLintQdsPlugin::registerPasses(PassManager *manager, const Element &rootElement)
{
    if (!rootElement.filePath().endsWith(u".ui.qml"))
        return;

    manager->registerPropertyPass(std::make_shared<FunctionCallValidator>(manager),
                                  QAnyStringView(), QAnyStringView());
    manager->registerPropertyPass(std::make_shared<QdsBindingValidator>(manager, rootElement),
                                  QAnyStringView(), QAnyStringView());
    manager->registerPropertyPass(std::make_unique<QQmlJSTranslationFunctionMismatchCheck>(manager),
                                  QString(), QString(), QString());
    manager->registerElementPass(std::make_unique<QdsElementValidator>(manager));
}

void FunctionCallValidator::onCall(const Element &element, const QString &propertyName,
                                   const Element &readScope, SourceLocation location)
{
    auto currentQmlScope = QQmlJSScope::findCurrentQMLScope(QQmlJSScope::scope(readScope));
    // TODO: we would benefit from some public additional public QQmlSA API here.
    //       This should be considered in the context of QTBUG-138360
    if (currentQmlScope && currentQmlScope->inherits(QQmlJSScope::scope(m_connectionsType)))
        return;

    // all math functions are allowed
    const Element globalJSObject = resolveBuiltinType(u"GlobalObject");
    const Element mathObjectType = globalJSObject.property(u"Math"_s).type();
    if (element.inherits(mathObjectType))
        return;

    const Element qjsValue = resolveBuiltinType(u"QJSValue");
    if (element.inherits(qjsValue)) {
        // Workaround because the Date method has methods and those are only represented in
        // QQmlJSTypePropagator as QJSValue.
        // This is an overapproximation and might flag unrelated methods with the same name as ok
        // even if they are not, but this is better than bogus warnings about the valid Date methods.
        const std::array<QStringView, 4> dateMethodmethods{ u"now", u"parse", u"prototype",
                                                            u"UTC" };
        if (auto it = std::find(dateMethodmethods.cbegin(), dateMethodmethods.cend(), propertyName);
            it != dateMethodmethods.cend())
            return;
    }

    static const std::vector<std::pair<Element, std::unordered_set<QString>>>
            whiteListedFunctions = {
                { Element(),
                  {
                          // used on JS objects and many other types
                          u"valueOf"_s,
                          u"toString"_s,
                          u"toLocaleString"_s,
                  } },
                { globalJSObject,
                  {
                          u"isNaN"_s, u"isFinite"_s,
                          u"qsTr"_s, u"qsTrId"_s, u"qsTranslate"_s,
                          u"QT_TRANSLATE_NOOP"_s, u"QT_TRID_NOOP"_s, u"QT_TR_NOOP"_s,
                  }
                },
                { resolveBuiltinType(u"ArrayPrototype"_s), { u"indexOf"_s, u"lastIndexOf"_s } },
                { resolveBuiltinType(u"NumberPrototype"_s),
                  {
                          u"isNaN"_s,
                          u"isFinite"_s,
                          u"toFixed"_s,
                          u"toExponential"_s,
                          u"toPrecision"_s,
                          u"isInteger"_s,
                  } },
                { resolveBuiltinType(u"StringPrototype"_s),
                  {
                          u"arg"_s,
                          u"toLowerCase"_s,
                          u"toLocaleLowerCase"_s,
                          u"toUpperCase"_s,
                          u"toLocaleUpperCase"_s,
                          u"substring"_s,
                          u"charAt"_s,
                          u"charCodeAt"_s,
                          u"concat"_s,
                          u"includes"_s,
                          u"endsWith"_s,
                          u"indexOf"_s,
                          u"lastIndexOf"_s,
                  } },
                { resolveType(u"QtQml"_s, u"Qt"_s),
                  { u"lighter"_s, u"darker"_s, u"rgba"_s, u"tint"_s, u"hsla"_s, u"hsva"_s,
                    u"point"_s, u"rect"_s, u"size"_s, u"vector2d"_s, u"vector3d"_s, u"vector4d"_s,
                    u"quaternion"_s, u"matrix4x4"_s, u"formatDate"_s, u"formatDateTime"_s,
                    u"formatTime"_s, u"resolvedUrl"_s } },
            };

    for (const auto &[currentElement, methods] : whiteListedFunctions) {
        if ((!currentElement || element.inherits(currentElement)) && methods.count(propertyName)) {
            return;
        }
    }

    // all other functions are forbidden
    emitWarning(u"Arbitrary functions and function calls outside of a Connections object are not "
                u"supported in a UI file (.ui.qml)",
                ErrFunctionsNotSupportedInQmlUi, location);
}

QdsElementValidator::QdsElementValidator(PassManager *manager) : ElementPass(manager)
{
    auto loadTypes = [&manager, this](QSpan<const UnsupportedName> names, QSpan<Element> output) {
        for (qsizetype i = 0; i < qsizetype(names.size()); ++i) {
            if (!manager->hasImportedModule(names[i].first))
                continue;
            output[i] = resolveType(names[i].first, names[i].second);
        }
    };
    loadTypes(s_unsupportedElementNames, m_unsupportedElements);
    loadTypes(s_unsupportedRootNames, m_unsupportedRootElements);
    m_qtObject = resolveType("QtQml"_L1, "QtObject"_L1);
    m_supportFunctions = { resolveType("QtQml"_L1, "Connections"_L1),
                           resolveType("QtQuick"_L1, "ScriptAction"_L1) };
}

void QdsElementValidator::complainAboutFunctions(const Element &element)
{
    for (const auto &method : element.ownMethods()) {
        if (method.methodType() != QQmlSA::MethodType::Method)
            continue;

        emitWarning(
                u"Arbitrary functions and function calls outside of a Connections object are not "
                u"supported in a UI file (.ui.qml)",
                ErrFunctionsNotSupportedInQmlUi, method.sourceLocation());
    }

    for (const auto &binding : element.ownPropertyBindings()) {
        if (!binding.hasFunctionScriptValue())
            continue;
        emitWarning(u"Arbitrary functions and function calls outside of a Connections object "
                    u"are not "
                    u"supported in a UI file (.ui.qml)",
                    ErrFunctionsNotSupportedInQmlUi, binding.sourceLocation());
    }
}

void QdsElementValidator::run(const Element &element)
{
    enum WarningType { ForElements, ForRootElements };
    auto warnIfElementIsUnsupported = [this, &element](WarningType warningType) {
        QSpan<const Element> unsupportedComponents = warningType == ForElements
                ? QSpan<const Element>(m_unsupportedElements)
                : QSpan<const Element>(m_unsupportedRootElements);
        const QStringView message = warningType == ForElements
                ? u"This type (%1) is not supported in a UI file (.ui.qml)."_sv
                : u"This type (%1) is not supported as a root element of a UI file (.ui.qml)."_sv;
        const LoggerWarningId &id = warningType == ForElements ? ErrUnsupportedTypeInQmlUi
                                                               : ErrUnsupportedRootTypeInQmlUi;

        for (const auto &unsupportedElement : unsupportedComponents) {
            if (!unsupportedElement || !element.inherits(unsupportedElement))
                continue;

            emitWarning(message.arg(element.baseTypeName()), id, element.sourceLocation());
            break;
        }

        // special case: we don't want to warn on types indirectly inheriting from QtObject, for
        // example Item.
        if (warningType == ForRootElements && element.baseType() == m_qtObject)
            emitWarning(message.arg(element.baseTypeName()), id, element.sourceLocation());
    };

    if (element.isFileRootComponent())
        warnIfElementIsUnsupported(ForRootElements);
    warnIfElementIsUnsupported(ForElements);

    if (QString id = resolveElementToId(element, element); !id.isEmpty()) {
        static constexpr std::array unsupportedNames = {
            "action"_L1,     "alias"_L1,    "anchors"_L1,   "as"_L1,          "baseState"_L1,
            "bool"_L1,       "border"_L1,   "bottom"_L1,    "break"_L1,       "case"_L1,
            "catch"_L1,      "clip"_L1,     "color"_L1,     "continue"_L1,    "data"_L1,
            "date"_L1,       "debugger"_L1, "default"_L1,   "delete"_L1,      "do"_L1,
            "double"_L1,     "else"_L1,     "enabled"_L1,   "enumeration"_L1, "finally"_L1,
            "flow"_L1,       "focus"_L1,    "font"_L1,      "for"_L1,         "function"_L1,
            "height"_L1,     "id"_L1,       "if"_L1,        "import"_L1,      "in"_L1,
            "instanceof"_L1, "int"_L1,      "item"_L1,      "layer"_L1,       "left"_L1,
            "list"_L1,       "margin"_L1,   "matrix4x4"_L1, "new"_L1,         "opacity"_L1,
            "padding"_L1,    "parent"_L1,   "point"_L1,     "print"_L1,       "quaternion"_L1,
            "real"_L1,       "rect"_L1,     "return"_L1,    "right"_L1,       "scale"_L1,
            "shaderInfo"_L1, "size"_L1,     "source"_L1,    "sprite"_L1,      "spriteSequence"_L1,
            "state"_L1,      "string"_L1,   "switch"_L1,    "text"_L1,        "texture"_L1,
            "this"_L1,       "throw"_L1,    "time"_L1,      "top"_L1,         "try"_L1,
            "typeof"_L1,     "url"_L1,      "var"_L1,       "variant"_L1,     "vector"_L1,
            "vector2d"_L1,   "vector3d"_L1, "vector4d"_L1,  "visible"_L1,     "void"_L1,
            "while"_L1,      "width"_L1,    "with"_L1,      "x"_L1,           "y"_L1,
            "z"_L1,
        };

        Q_ASSERT(std::is_sorted(unsupportedNames.begin(), unsupportedNames.end()));
        if (std::binary_search(unsupportedNames.cbegin(), unsupportedNames.cend(), id)) {
            emitWarning(
                    u"This id (%1) might be ambiguous and is not supported in a UI file (.ui.qml)."_s
                            .arg(id),
                    ErrInvalidIdeInVisualDesigner, element.idSourceLocation());
        }
    }

    if (std::none_of(m_supportFunctions.cbegin(), m_supportFunctions.cend(),
                     [&element](const Element &base) { return base && element.inherits(base); })) {
        complainAboutFunctions(element);
    }
}

QT_END_NAMESPACE

#include "moc_qdslintplugin.cpp"
