/**
 * Compile-time checks associated with the @mustuse attribute.
 *
 * Copyright: Copyright (C) 2022 by The D Language Foundation, All Rights Reserved
 * License:   $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
 * Source:    $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/mustuse.d, _mustuse.d)
 * Documentation:  https://dlang.org/phobos/dmd_mustuse.html
 * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/mustuse.d
 */

module dmd.mustuse;

import dmd.dscope;
import dmd.dsymbol;
import dmd.expression;
import dmd.globals;
import dmd.identifier;

// Used in isIncrementOrDecrement
private static const StringExp plusPlus, minusMinus;

// Loc.initial cannot be used in static initializers, so
// these need a static constructor.
static this()
{
    plusPlus = new StringExp(Loc.initial, "++");
    minusMinus = new StringExp(Loc.initial, "--");
}

/**
 * Check whether discarding an expression would violate the requirements of
 * @mustuse. If so, emit an error.
 *
 * Params:
 *   e = the expression to check
 *   sc = scope in which `e` was semantically analyzed
 *
 * Returns: true on error, false on success.
 */
bool checkMustUse(Expression e, Scope* sc)
{
    import dmd.id : Id;

    assert(e.type);
    if (auto sym = e.type.toDsymbol(sc))
    {
        auto sd = sym.isStructDeclaration();
        // isStructDeclaration returns non-null for both structs and unions
        if (sd && hasMustUseAttribute(sd, sc) && !isAssignment(e) && !isIncrementOrDecrement(e))
        {
            e.error("ignored value of `@%s` type `%s`; prepend a `cast(void)` if intentional",
                Id.udaMustUse.toChars(), e.type.toPrettyChars(true));
            return true;
        }
    }
    return false;
}

/**
 * Called from a symbol's semantic to check for reserved usage of @mustuse.
 *
 * If such usage is found, emits an errror.
 *
 * Params:
 *   sym = symbol to check
 */
void checkMustUseReserved(Dsymbol sym)
{
    import dmd.attrib : foreachUdaNoSemantic;
    import dmd.errors : error;
    import dmd.id : Id;

    // Can't use foreachUda (and by extension hasMustUseAttribute) while
    // semantic analysis of `sym` is still in progress
    foreachUdaNoSemantic(sym, (exp) {
        if (isMustUseAttribute(exp))
        {
            if (sym.isFuncDeclaration())
            {
                error(sym.loc, "`@%s` on functions is reserved for future use",
                    Id.udaMustUse.toChars());
                sym.errors = true;
            }
            else if (sym.isClassDeclaration() || sym.isEnumDeclaration())
            {
                error(sym.loc, "`@%s` on `%s` types is reserved for future use",
                    Id.udaMustUse.toChars(), sym.kind());
                sym.errors = true;
            }
        }
        return 0; // continue
    });
}

/**
 * Returns: true if the given expression is an assignment, either simple (a = b)
 * or compound (a += b, etc).
 */
private bool isAssignment(Expression e)
{
    if (e.isAssignExp || e.isBinAssignExp)
        return true;
    if (auto ce = e.isCallExp())
    {
        if (auto fd = ce.f)
        {
            auto id = fd.ident;
            if (id && isAssignmentOpId(id))
                return true;
        }
    }
    return false;
}

/**
 * Returns: true if id is the identifier of an assignment operator overload.
 */
private bool isAssignmentOpId(Identifier id)
{
    import dmd.id : Id;

    return id == Id.assign
        || id == Id.addass
        || id == Id.subass
        || id == Id.mulass
        || id == Id.divass
        || id == Id.modass
        || id == Id.andass
        || id == Id.orass
        || id == Id.xorass
        || id == Id.shlass
        || id == Id.shrass
        || id == Id.ushrass
        || id == Id.catass
        || id == Id.indexass
        || id == Id.slice
        || id == Id.sliceass
        || id == Id.opOpAssign
        || id == Id.opIndexOpAssign
        || id == Id.opSliceOpAssign
        || id == Id.powass;
}

/**
 * Returns: true if the given expression is an increment (++) or decrement (--).
 */
private bool isIncrementOrDecrement(Expression e)
{
    import dmd.dtemplate : isExpression;
    import dmd.globals : Loc;
    import dmd.id : Id;
    import dmd.tokens : EXP;

    if (e.op == EXP.plusPlus
        || e.op == EXP.minusMinus
        || e.op == EXP.prePlusPlus
        || e.op == EXP.preMinusMinus)
        return true;
    if (auto call = e.isCallExp())
    {
        // Check for overloaded preincrement
        // e.g., a.opUnary!"++"
        if (auto fd = call.f)
        {
            if (fd.ident == Id.opUnary && fd.parent)
            {
                if (auto ti = fd.parent.isTemplateInstance())
                {
                    auto tiargs = ti.tiargs;
                    if (tiargs && tiargs.length >= 1)
                    {
                        if (auto argExp = (*tiargs)[0].isExpression())
                        {
                            auto op = argExp.isStringExp();
                            if (op && (op.compare(plusPlus) == 0 || op.compare(minusMinus) == 0))
                                return true;
                        }
                    }
                }
            }
        }
    }
    else if (auto comma = e.isCommaExp())
    {
        // Check for overloaded postincrement
        // e.g., (auto tmp = a, ++a, tmp)
        if (comma.e1)
        {
            if (auto left = comma.e1.isCommaExp())
            {
                if (auto middle = left.e2)
                {
                    if (middle && isIncrementOrDecrement(middle))
                        return true;
                }
            }
        }
    }
    return false;
}

/**
 * Returns: true if the given symbol has the @mustuse attribute.
 */
private bool hasMustUseAttribute(Dsymbol sym, Scope* sc)
{
    import dmd.attrib : foreachUda;

    bool result = false;

    foreachUda(sym, sc, (Expression uda) {
        if (isMustUseAttribute(uda))
        {
            result = true;
            return 1; // break
        }
        return 0; // continue
    });

    return result;
}

/**
 * Returns: true if the given expression is core.attribute.mustuse.
 */
private bool isMustUseAttribute(Expression e)
{
    import dmd.attrib : isCoreUda;
    import dmd.id : Id;

    // Logic based on dmd.objc.Supported.declaredAsOptionalCount
    auto typeExp = e.isTypeExp;
    if (!typeExp)
        return false;

    auto typeEnum = typeExp.type.isTypeEnum();
    if (!typeEnum)
        return false;

    if (isCoreUda(typeEnum.sym, Id.udaMustUse))
        return true;

    return false;
}
