all files / babel-plugin-angular-inject/ index.js

88.68% Statements 47/53
83.02% Branches 44/53
100% Functions 6/6
88.46% Lines 46/52
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173                                  33×                             36×     36× 18× 18× 18×                     38× 38×   11×         12×                                                                                               67× 65×                       57×           39×   18×     18×         14×       14×   12×   12×        
/**
 * Supports es6 with the es2015 preset :D
 *
 * Super useful:
 *  http://astexplorer.net/
 */
"use strict";
 
/**
 * List of functions we should intercept
 * @type {string[]}
 */
const injectables = ["controller", "directive", "factory", "filter", "config", "run"],
    /**
     * Returns the callee function name
     * @param node
     * @returns {String}
     */
    calleeName = (node) => node.callee.type === "MemberExpression" ? node.callee.property.name : null,
    /**
     * Map of CallExpression's to ignore
     * @type {WeakMap}
     */
    ignoreMap = new WeakMap();
 
/**
 * Check whether a callee is a child of a module function
 * @param object
 * @param bindings
 * @returns {boolean}
 */
function childOfModule (object, bindings) {
    // Map stored variables onto their declared values
    //
    Iif (object.type === "Identifier" && bindings[object.name])
        object = bindings[object.name].path.node.init;
 
    if (object.callee && object.callee.type === "MemberExpression" && object.callee.property.name === "module")
        return true;
    else Eif (object.callee && object.callee.object && object.callee.object)
        return childOfModule(object.callee.object, bindings);
    else
        return false;
}
 
/**
 * Function to trace an argument back to the original referenced function
 * @param object
 * @param scope
 * @returns {*}
 */
function followTheYellowBrickRoad (object, scope) {
    try {
        switch (object.type) {
            case "Identifier":
                return followTheYellowBrickRoad(scope.bindings[object.name].path.node, scope.bindings[object.name].scope);
 
            case "FunctionExpression":
            case "FunctionDeclaration":
            case "ArrowFunctionExpression":
                return object;
 
            case "CallExpression":
                // Check whether this is a converted class
                //
                Eif (object.callee && object.callee.body
                    && object.callee.body.type === "BlockStatement")
                    return followPossibleTranspiledClass(object.callee.body.body);
                else
                    return null;
 
            case "VariableDeclaration":
                return followTheYellowBrickRoad(object.declarations[0].init, scope);
 
            case "VariableDeclarator":
                return followTheYellowBrickRoad(object.init, scope);
 
            case "MemberExpression":
                let mappedValue = scope.bindings[object.object.name].path.node.init.properties
                    .filter(prop => prop.key.name === object.property.name)
                    .map(prop => prop.value);
                Eif (mappedValue.length > 0)
                    return followTheYellowBrickRoad(mappedValue[0], scope.bindings[object.object.name].path.scope);
 
            default:
                return null;
        }
    } catch (e) {
        console.error(e);
        return null;
    }
}
 
/**
 * Digs out the function from a possibly transpiled class (from es6 -> es5)
 * @param object
 * @returns {*}
 */
function followPossibleTranspiledClass (object) {
    Iif (object.length !== 3)
        return null;
 
    // Check first node is a function declaration and last is a return
    //
    Iif (object[0].type !== "FunctionDeclaration" || object[2].type !== "ReturnStatement")
        return null;
 
    // Check first function is same function passed to return
    //
    Eif (object[0].id.name === object[2].argument.name)
        return object[0];
 
    // Else, no idea, ignore it
    //
    return null;
}
 
module.exports = function angularInject (babel) {
    let t = babel.types;
    return {
        visitor: {
            /**
             * Build a list of user defined $inject arrays and ignore them later
             * @param path
             * @returns {boolean}
             * @constructor
             */
            MemberExpression (path) {
                if (path.node.property.name !== "$inject")
                    return false;
 
                // Store reference to identifier
                //
                ignoreMap.set(path.scope.bindings[path.parent.left.object.name], true);
            },
            /**
             * CallExpression processor
             * @param path
             * @returns {boolean}
             * @constructor
             */
            CallExpression (path) {
                if (// Secondly, check arguments list contains at least two
                    path.node["arguments"].length < 2 ||
                    // Thirdly, check callee exists in allowed injectables list
                    injectables.indexOf(calleeName(path.node)) === -1 ||
                    // finally, check expressions stems from a module() invocation
                    !childOfModule(path.node, path.scope.bindings))
                    return false;
 
                let fn = path.node["arguments"][1],
                    fnNode = followTheYellowBrickRoad(fn, path.scope);
 
                if (fnNode === null)
                    return false;
 
 
                // Build array of angular references
                //
                let references = (fnNode.params || []).map((param) => t.stringLiteral(param.name));
 
                // Check if $inject is defined, if so, ignore
                //
                if (ignoreMap.has(path.scope.bindings[fn.name]))
                    return false;
 
                references.push(fn);
 
                path.node["arguments"][1] = t.arrayExpression(references);
            }
        }
    };
};