The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#define NEED_PL_parser
#include "ppport.h"

#if (PERL_BCDVERSION >= 0x5009005)
#define PL_lex_inwhat (PL_parser->lex_inwhat)
#endif

#include "hook_op_check.h"

#include "ptable.h"

#if (PERL_VERSION < 10)
#define NCA_PMOP_STASHSTARTU(o) (o->op_pmreplstart)
#else
#define NCA_PMOP_STASHSTARTU(o) (o->op_pmstashstartu.op_pmreplstart)
#endif


#define MG_UNSTRICT ((U16) (0xaffe))
#define MG_UNCALL   ((U16) (0xafff))

#define enabled(u) S_enabled (aTHX_ u)

typedef struct user_data_St {
    char *file;
    SV *cb;
    hook_op_check_id gv;
    hook_op_check_id entersub;
} user_data_t;

STATIC void (*orig_peep)(pTHX_ OP *op);

STATIC SV *
invoke_callback (pTHX_ SV *cb, SV *name)
{
    dSP;
    int count;
    SV *ret;

    ENTER;
    SAVETMPS;

    PUSHMARK (SP);
    XPUSHs (name);
    PUTBACK;

    count = call_sv (cb, G_SCALAR);

    SPAGAIN;

    if (count != 1) {
        croak ("namespace::alias callback didn't return a single argument");
    }

    ret = SvREFCNT_inc (POPs);

    PUTBACK;
    FREETMPS;
    LEAVE;

    return ret;
}

STATIC void
tag (OP *op, int type, void *data)
{
    SV *sv;
    MAGIC *mg;

    assert (op->op_type == OP_CONST || op->op_type == OP_GV);

    if (op->op_type == OP_CONST) {
        sv = cSVOPx (op)->op_sv;
    }
    else {
        sv = (SV *)cGVOPx_gv (op);
    }

    mg = sv_magicext (sv, NULL, PERL_MAGIC_ext, NULL, NULL, 0);
    mg->mg_private = type;
    mg->mg_ptr = data;
}

STATIC int
tagged (OP *op, int type, void **data)
{
    SV *sv;
    MAGIC *mg;

    assert (op->op_type == OP_CONST || op->op_type == OP_GV);

    if (op->op_type == OP_CONST) {
        sv = cSVOPx (op)->op_sv;
    }
    else {
        sv = (SV *)cGVOPx_gv (op);
    }

    if (SvTYPE (sv) < SVt_PVMG) {
        return 0;
    }

    for (mg = SvMAGIC (sv); mg; mg = mg->mg_moremagic) {
        switch (mg->mg_type) {
            case PERL_MAGIC_ext:
                if (mg->mg_private == type) {
                    if (data) {
                        *data = mg->mg_ptr;
                    }
                    return 1;
                }
                break;
            default:
                break;
        }
    }

    return 0;
}

STATIC int
S_enabled (pTHX_ user_data_t *ud)
{
    char *file = CopFILE (PL_curcop);

    if (file && strEQ (file, ud->file)) {
        return 1;
    }

    return 0;
}

STATIC OP *
check_alias (pTHX_ OP *op, void *user_data)
{
    user_data_t *ud = (user_data_t *)user_data;
    SV *name = cSVOPx (op)->op_sv;
    SV *replacement;

    if (!enabled (ud)) {
        return op;
    }

    if (!SvPOK (name)) {
        return op;
    }

    if (PL_lex_stuff) {
        return op;
    }

    switch (PL_lex_inwhat) {
        case OP_QR:
        case OP_MATCH:
        case OP_SUBST:
        case OP_TRANS:
        case OP_BACKTICK:
        case OP_STRINGIFY:
            return op;
            break;
        default:
            break;
    }

    /*
     * We explicitly don't handle the case of
     *
     *   MyAlias
     *   => 42
     *
     * here. We still call the alias expansion callback for that, but for some
     * obscure reason, perl won't pick up the replaced sv, so we don't need to
     * bother with scanning ahead in the linestr.
     */
    if (strnEQ (PL_bufptr, SvPV_nolen (name), SvCUR (name))) {
        char *s = PL_bufptr;
        s += SvCUR (name);
        while (s < PL_bufend && isSPACE(*s)) {
            s++;
        }

        if ((PL_bufend - s) >= 2 && strnEQ(s, "=>", 2)) {
            return op;
        }
    }

    replacement = invoke_callback (aTHX_ ud->cb, name);
    if (!SvTRUE (replacement)) {
        SvREFCNT_dec (replacement);
        return op;
    }

    /*
     * Modify name in place rather than putting replacement into the op
     * in its stead, because the core (5.11.2+) may be relying on the
     * name SV living in order to put it into another op.
     */
    sv_setsv(name, replacement);
    SvREFCNT_dec(replacement);

    tag (op, MG_UNSTRICT, NULL);

    return op;
}

typedef void (*cb_t)(pTHX_ OP *o);

STATIC void
_walk_optree (pTHX_ OP *o, cb_t cb, ptable *visited)
{
    if (!o || ptable_fetch(visited, o))
        return;

    for (; o; o = o->op_next) {
        ptable_store(visited, o, o);

        switch (PL_opargs[o->op_type] & OA_CLASS_MASK) {
        case OA_LOOP:
            _walk_optree(aTHX_ cLOOPo->op_redoop, cb, visited);
            _walk_optree(aTHX_ cLOOPo->op_nextop, cb, visited);
            _walk_optree(aTHX_ cLOOPo->op_lastop, cb, visited);
            break;
        case OA_LOGOP:
            _walk_optree(aTHX_ cLOGOPo->op_other, cb, visited);
            break;
        case OA_PMOP:
            if (o->op_type == OP_SUBST)
                _walk_optree(aTHX_ NCA_PMOP_STASHSTARTU(cPMOPo), cb, visited);
            break;
        }

        cb(aTHX_ o);
    }
}

STATIC void
walk_optree (pTHX_ OP *o, cb_t cb)
{
    ptable *visited_ops = ptable_new();
    _walk_optree(aTHX_ o, cb, visited_ops);
    ptable_free(visited_ops);
}

STATIC void
unstrict (pTHX_ OP *o) {
    if (o->op_type == OP_CONST) {
        if (tagged (o, MG_UNSTRICT, NULL))
            o->op_private &= ~OPpCONST_STRICT;
    }
}

STATIC void
peep_unstrict (pTHX_ OP *op)
{
    walk_optree(aTHX_ op, unstrict);
    orig_peep(aTHX_ op);
}

STATIC OP *
check_gv (pTHX_ OP *op, void *user_data)
{
    user_data_t *ud = (user_data_t *)user_data;
    GV *gv;
    SV *name, *replacement;

    if (!enabled (ud)) {
        return op;
    }

    gv = cGVOPx_gv (op);
    if (!gv || !GvSTASH (gv)) {
        return op;
    }

    name = newSVpv (HvNAME (GvSTASH (gv)), 0);
    sv_catpvs (name, "::");
    sv_catpv (name, GvNAME (gv));

    replacement = invoke_callback (aTHX_ ud->cb, name);
    if (!SvTRUE (replacement)) {
        SvREFCNT_dec (replacement);
        return op;
    }

    tag (op, MG_UNCALL, replacement);

    return op;
}

STATIC OP *
check_entersub (pTHX_ OP *op, void *user_data)
{
    user_data_t *ud = (user_data_t *)user_data;
    OP *prev = ((cUNOPx (op)->op_first->op_sibling)
        ? cUNOPx (op) : ((UNOP *)cUNOPx (op)->op_first))->op_first;
    OP *op2 = prev->op_sibling;
    OP *cvop, *gvop;
    SV *replacement;

    if (!enabled (ud)) {
        return op;
    }

    for (cvop = op2; cvop->op_sibling; cvop = cvop->op_sibling) ;

    if (cvop->op_type != OP_NULL) {
        return op;
    }

    gvop = cUNOPx (cvop)->op_first;
    if (gvop->op_type != OP_GV) {
        return op;
    }

    if (!tagged (gvop, MG_UNCALL, (void **)&replacement)) {
        return op;
    }

    op_free (op);

    return newSVOP (OP_CONST, 0, replacement);
}

MODULE = namespace::alias  PACKAGE = namespace::alias

PROTOTYPES: DISABLE

hook_op_check_id
setup (class, file, cb)
        char *file
        SV *cb
    PREINIT:
        user_data_t *ud;
    INIT:
        if (!SvROK (cb) || SvTYPE (SvRV (cb)) != SVt_PVCV) {
            croak ("callback is not a code reference");
        }

        Newx (ud, 1, user_data_t);
        ud->file = strdup (file);
        ud->cb = newSVsv (cb);
    CODE:
        ud->entersub = hook_op_check (OP_ENTERSUB, check_entersub, ud);
        ud->gv = hook_op_check (OP_GV, check_gv, ud);
        RETVAL = hook_op_check (OP_CONST, check_alias, ud);
    OUTPUT:
        RETVAL

void
teardown (class, hook)
        hook_op_check_id hook
    PREINIT:
        user_data_t *ud;
    CODE:
        ud = (user_data_t *)hook_op_check_remove (OP_CONST, hook);
        (void)hook_op_check_remove (OP_GV, ud->gv);
        (void)hook_op_check_remove (OP_ENTERSUB, ud->entersub);
        SvREFCNT_dec (ud->cb);
        free (ud->file);
        Safefree (ud);

BOOT:
    orig_peep = PL_peepp;
    PL_peepp = peep_unstrict;