• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2  Copyright (C) 2015 Yusuke Suzuki <utatane.tea@gmail.com>
3
4  Redistribution and use in source and binary forms, with or without
5  modification, are permitted provided that the following conditions are met:
6
7    * Redistributions of source code must retain the above copyright
8      notice, this list of conditions and the following disclaimer.
9    * Redistributions in binary form must reproduce the above copyright
10      notice, this list of conditions and the following disclaimer in the
11      documentation and/or other materials provided with the distribution.
12
13  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16  ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
17  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
22  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23*/
24"use strict";
25
26/* eslint-disable no-underscore-dangle */
27/* eslint-disable no-undefined */
28
29const Syntax = require("estraverse").Syntax;
30
31const Reference = require("./reference");
32const Variable = require("./variable");
33const Definition = require("./definition").Definition;
34const assert = require("assert");
35
36/**
37 * Test if scope is struct
38 * @param {Scope} scope - scope
39 * @param {Block} block - block
40 * @param {boolean} isMethodDefinition - is method definition
41 * @param {boolean} useDirective - use directive
42 * @returns {boolean} is strict scope
43 */
44function isStrictScope(scope, block, isMethodDefinition, useDirective) {
45    let body;
46
47    // When upper scope is exists and strict, inner scope is also strict.
48    if (scope.upper && scope.upper.isStrict) {
49        return true;
50    }
51
52    if (isMethodDefinition) {
53        return true;
54    }
55
56    if (scope.type === "class" || scope.type === "module") {
57        return true;
58    }
59
60    if (scope.type === "block" || scope.type === "switch") {
61        return false;
62    }
63
64    if (scope.type === "function") {
65        if (block.type === Syntax.ArrowFunctionExpression && block.body.type !== Syntax.BlockStatement) {
66            return false;
67        }
68
69        if (block.type === Syntax.Program) {
70            body = block;
71        } else {
72            body = block.body;
73        }
74
75        if (!body) {
76            return false;
77        }
78    } else if (scope.type === "global") {
79        body = block;
80    } else {
81        return false;
82    }
83
84    // Search 'use strict' directive.
85    if (useDirective) {
86        for (let i = 0, iz = body.body.length; i < iz; ++i) {
87            const stmt = body.body[i];
88
89            if (stmt.type !== Syntax.DirectiveStatement) {
90                break;
91            }
92            if (stmt.raw === "\"use strict\"" || stmt.raw === "'use strict'") {
93                return true;
94            }
95        }
96    } else {
97        for (let i = 0, iz = body.body.length; i < iz; ++i) {
98            const stmt = body.body[i];
99
100            if (stmt.type !== Syntax.ExpressionStatement) {
101                break;
102            }
103            const expr = stmt.expression;
104
105            if (expr.type !== Syntax.Literal || typeof expr.value !== "string") {
106                break;
107            }
108            if (expr.raw !== null && expr.raw !== undefined) {
109                if (expr.raw === "\"use strict\"" || expr.raw === "'use strict'") {
110                    return true;
111                }
112            } else {
113                if (expr.value === "use strict") {
114                    return true;
115                }
116            }
117        }
118    }
119    return false;
120}
121
122/**
123 * Register scope
124 * @param {ScopeManager} scopeManager - scope manager
125 * @param {Scope} scope - scope
126 * @returns {void}
127 */
128function registerScope(scopeManager, scope) {
129    scopeManager.scopes.push(scope);
130
131    const scopes = scopeManager.__nodeToScope.get(scope.block);
132
133    if (scopes) {
134        scopes.push(scope);
135    } else {
136        scopeManager.__nodeToScope.set(scope.block, [scope]);
137    }
138}
139
140/**
141 * Should be statically
142 * @param {Object} def - def
143 * @returns {boolean} should be statically
144 */
145function shouldBeStatically(def) {
146    return (
147        (def.type === Variable.ClassName) ||
148        (def.type === Variable.Variable && def.parent.kind !== "var")
149    );
150}
151
152/**
153 * @class Scope
154 */
155class Scope {
156    constructor(scopeManager, type, upperScope, block, isMethodDefinition) {
157
158        /**
159         * One of 'module', 'block', 'switch', 'function', 'catch', 'with', 'function', 'class', 'global'.
160         * @member {String} Scope#type
161         */
162        this.type = type;
163
164        /**
165         * The scoped {@link Variable}s of this scope, as <code>{ Variable.name
166         * : Variable }</code>.
167         * @member {Map} Scope#set
168         */
169        this.set = new Map();
170
171        /**
172         * The tainted variables of this scope, as <code>{ Variable.name :
173         * boolean }</code>.
174         * @member {Map} Scope#taints */
175        this.taints = new Map();
176
177        /**
178         * Generally, through the lexical scoping of JS you can always know
179         * which variable an identifier in the source code refers to. There are
180         * a few exceptions to this rule. With 'global' and 'with' scopes you
181         * can only decide at runtime which variable a reference refers to.
182         * Moreover, if 'eval()' is used in a scope, it might introduce new
183         * bindings in this or its parent scopes.
184         * All those scopes are considered 'dynamic'.
185         * @member {boolean} Scope#dynamic
186         */
187        this.dynamic = this.type === "global" || this.type === "with";
188
189        /**
190         * A reference to the scope-defining syntax node.
191         * @member {espree.Node} Scope#block
192         */
193        this.block = block;
194
195        /**
196         * The {@link Reference|references} that are not resolved with this scope.
197         * @member {Reference[]} Scope#through
198         */
199        this.through = [];
200
201        /**
202         * The scoped {@link Variable}s of this scope. In the case of a
203         * 'function' scope this includes the automatic argument <em>arguments</em> as
204         * its first element, as well as all further formal arguments.
205         * @member {Variable[]} Scope#variables
206         */
207        this.variables = [];
208
209        /**
210         * Any variable {@link Reference|reference} found in this scope. This
211         * includes occurrences of local variables as well as variables from
212         * parent scopes (including the global scope). For local variables
213         * this also includes defining occurrences (like in a 'var' statement).
214         * In a 'function' scope this does not include the occurrences of the
215         * formal parameter in the parameter list.
216         * @member {Reference[]} Scope#references
217         */
218        this.references = [];
219
220        /**
221         * For 'global' and 'function' scopes, this is a self-reference. For
222         * other scope types this is the <em>variableScope</em> value of the
223         * parent scope.
224         * @member {Scope} Scope#variableScope
225         */
226        this.variableScope =
227            (this.type === "global" || this.type === "function" || this.type === "module") ? this : upperScope.variableScope;
228
229        /**
230         * Whether this scope is created by a FunctionExpression.
231         * @member {boolean} Scope#functionExpressionScope
232         */
233        this.functionExpressionScope = false;
234
235        /**
236         * Whether this is a scope that contains an 'eval()' invocation.
237         * @member {boolean} Scope#directCallToEvalScope
238         */
239        this.directCallToEvalScope = false;
240
241        /**
242         * @member {boolean} Scope#thisFound
243         */
244        this.thisFound = false;
245
246        this.__left = [];
247
248        /**
249         * Reference to the parent {@link Scope|scope}.
250         * @member {Scope} Scope#upper
251         */
252        this.upper = upperScope;
253
254        /**
255         * Whether 'use strict' is in effect in this scope.
256         * @member {boolean} Scope#isStrict
257         */
258        this.isStrict = isStrictScope(this, block, isMethodDefinition, scopeManager.__useDirective());
259
260        /**
261         * List of nested {@link Scope}s.
262         * @member {Scope[]} Scope#childScopes
263         */
264        this.childScopes = [];
265        if (this.upper) {
266            this.upper.childScopes.push(this);
267        }
268
269        this.__declaredVariables = scopeManager.__declaredVariables;
270
271        registerScope(scopeManager, this);
272    }
273
274    __shouldStaticallyClose(scopeManager) {
275        return (!this.dynamic || scopeManager.__isOptimistic());
276    }
277
278    __shouldStaticallyCloseForGlobal(ref) {
279
280        // On global scope, let/const/class declarations should be resolved statically.
281        const name = ref.identifier.name;
282
283        if (!this.set.has(name)) {
284            return false;
285        }
286
287        const variable = this.set.get(name);
288        const defs = variable.defs;
289
290        return defs.length > 0 && defs.every(shouldBeStatically);
291    }
292
293    __staticCloseRef(ref) {
294        if (!this.__resolve(ref)) {
295            this.__delegateToUpperScope(ref);
296        }
297    }
298
299    __dynamicCloseRef(ref) {
300
301        // notify all names are through to global
302        let current = this;
303
304        do {
305            current.through.push(ref);
306            current = current.upper;
307        } while (current);
308    }
309
310    __globalCloseRef(ref) {
311
312        // let/const/class declarations should be resolved statically.
313        // others should be resolved dynamically.
314        if (this.__shouldStaticallyCloseForGlobal(ref)) {
315            this.__staticCloseRef(ref);
316        } else {
317            this.__dynamicCloseRef(ref);
318        }
319    }
320
321    __close(scopeManager) {
322        let closeRef;
323
324        if (this.__shouldStaticallyClose(scopeManager)) {
325            closeRef = this.__staticCloseRef;
326        } else if (this.type !== "global") {
327            closeRef = this.__dynamicCloseRef;
328        } else {
329            closeRef = this.__globalCloseRef;
330        }
331
332        // Try Resolving all references in this scope.
333        for (let i = 0, iz = this.__left.length; i < iz; ++i) {
334            const ref = this.__left[i];
335
336            closeRef.call(this, ref);
337        }
338        this.__left = null;
339
340        return this.upper;
341    }
342
343    // To override by function scopes.
344    // References in default parameters isn't resolved to variables which are in their function body.
345    __isValidResolution(ref, variable) { // eslint-disable-line class-methods-use-this, no-unused-vars
346        return true;
347    }
348
349    __resolve(ref) {
350        const name = ref.identifier.name;
351
352        if (!this.set.has(name)) {
353            return false;
354        }
355        const variable = this.set.get(name);
356
357        if (!this.__isValidResolution(ref, variable)) {
358            return false;
359        }
360        variable.references.push(ref);
361        variable.stack = variable.stack && ref.from.variableScope === this.variableScope;
362        if (ref.tainted) {
363            variable.tainted = true;
364            this.taints.set(variable.name, true);
365        }
366        ref.resolved = variable;
367
368        return true;
369    }
370
371    __delegateToUpperScope(ref) {
372        if (this.upper) {
373            this.upper.__left.push(ref);
374        }
375        this.through.push(ref);
376    }
377
378    __addDeclaredVariablesOfNode(variable, node) {
379        if (node === null || node === undefined) {
380            return;
381        }
382
383        let variables = this.__declaredVariables.get(node);
384
385        if (variables === null || variables === undefined) {
386            variables = [];
387            this.__declaredVariables.set(node, variables);
388        }
389        if (variables.indexOf(variable) === -1) {
390            variables.push(variable);
391        }
392    }
393
394    __defineGeneric(name, set, variables, node, def) {
395        let variable;
396
397        variable = set.get(name);
398        if (!variable) {
399            variable = new Variable(name, this);
400            set.set(name, variable);
401            variables.push(variable);
402        }
403
404        if (def) {
405            variable.defs.push(def);
406            this.__addDeclaredVariablesOfNode(variable, def.node);
407            this.__addDeclaredVariablesOfNode(variable, def.parent);
408        }
409        if (node) {
410            variable.identifiers.push(node);
411        }
412    }
413
414    __define(node, def) {
415        if (node && node.type === Syntax.Identifier) {
416            this.__defineGeneric(
417                node.name,
418                this.set,
419                this.variables,
420                node,
421                def
422            );
423        }
424    }
425
426    __referencing(node, assign, writeExpr, maybeImplicitGlobal, partial, init) {
427
428        // because Array element may be null
429        if (!node || node.type !== Syntax.Identifier) {
430            return;
431        }
432
433        // Specially handle like `this`.
434        if (node.name === "super") {
435            return;
436        }
437
438        const ref = new Reference(node, this, assign || Reference.READ, writeExpr, maybeImplicitGlobal, !!partial, !!init);
439
440        this.references.push(ref);
441        this.__left.push(ref);
442    }
443
444    __detectEval() {
445        let current = this;
446
447        this.directCallToEvalScope = true;
448        do {
449            current.dynamic = true;
450            current = current.upper;
451        } while (current);
452    }
453
454    __detectThis() {
455        this.thisFound = true;
456    }
457
458    __isClosed() {
459        return this.__left === null;
460    }
461
462    /**
463     * returns resolved {Reference}
464     * @method Scope#resolve
465     * @param {Espree.Identifier} ident - identifier to be resolved.
466     * @returns {Reference} reference
467     */
468    resolve(ident) {
469        let ref, i, iz;
470
471        assert(this.__isClosed(), "Scope should be closed.");
472        assert(ident.type === Syntax.Identifier, "Target should be identifier.");
473        for (i = 0, iz = this.references.length; i < iz; ++i) {
474            ref = this.references[i];
475            if (ref.identifier === ident) {
476                return ref;
477            }
478        }
479        return null;
480    }
481
482    /**
483     * returns this scope is static
484     * @method Scope#isStatic
485     * @returns {boolean} static
486     */
487    isStatic() {
488        return !this.dynamic;
489    }
490
491    /**
492     * returns this scope has materialized arguments
493     * @method Scope#isArgumentsMaterialized
494     * @returns {boolean} arguemnts materialized
495     */
496    isArgumentsMaterialized() { // eslint-disable-line class-methods-use-this
497        return true;
498    }
499
500    /**
501     * returns this scope has materialized `this` reference
502     * @method Scope#isThisMaterialized
503     * @returns {boolean} this materialized
504     */
505    isThisMaterialized() { // eslint-disable-line class-methods-use-this
506        return true;
507    }
508
509    isUsedName(name) {
510        if (this.set.has(name)) {
511            return true;
512        }
513        for (let i = 0, iz = this.through.length; i < iz; ++i) {
514            if (this.through[i].identifier.name === name) {
515                return true;
516            }
517        }
518        return false;
519    }
520}
521
522class GlobalScope extends Scope {
523    constructor(scopeManager, block) {
524        super(scopeManager, "global", null, block, false);
525        this.implicit = {
526            set: new Map(),
527            variables: [],
528
529            /**
530            * List of {@link Reference}s that are left to be resolved (i.e. which
531            * need to be linked to the variable they refer to).
532            * @member {Reference[]} Scope#implicit#left
533            */
534            left: []
535        };
536    }
537
538    __close(scopeManager) {
539        const implicit = [];
540
541        for (let i = 0, iz = this.__left.length; i < iz; ++i) {
542            const ref = this.__left[i];
543
544            if (ref.__maybeImplicitGlobal && !this.set.has(ref.identifier.name)) {
545                implicit.push(ref.__maybeImplicitGlobal);
546            }
547        }
548
549        // create an implicit global variable from assignment expression
550        for (let i = 0, iz = implicit.length; i < iz; ++i) {
551            const info = implicit[i];
552
553            this.__defineImplicit(info.pattern,
554                new Definition(
555                    Variable.ImplicitGlobalVariable,
556                    info.pattern,
557                    info.node,
558                    null,
559                    null,
560                    null
561                ));
562
563        }
564
565        this.implicit.left = this.__left;
566
567        return super.__close(scopeManager);
568    }
569
570    __defineImplicit(node, def) {
571        if (node && node.type === Syntax.Identifier) {
572            this.__defineGeneric(
573                node.name,
574                this.implicit.set,
575                this.implicit.variables,
576                node,
577                def
578            );
579        }
580    }
581}
582
583class ModuleScope extends Scope {
584    constructor(scopeManager, upperScope, block) {
585        super(scopeManager, "module", upperScope, block, false);
586    }
587}
588
589class FunctionExpressionNameScope extends Scope {
590    constructor(scopeManager, upperScope, block) {
591        super(scopeManager, "function-expression-name", upperScope, block, false);
592        this.__define(block.id,
593            new Definition(
594                Variable.FunctionName,
595                block.id,
596                block,
597                null,
598                null,
599                null
600            ));
601        this.functionExpressionScope = true;
602    }
603}
604
605class CatchScope extends Scope {
606    constructor(scopeManager, upperScope, block) {
607        super(scopeManager, "catch", upperScope, block, false);
608    }
609}
610
611class WithScope extends Scope {
612    constructor(scopeManager, upperScope, block) {
613        super(scopeManager, "with", upperScope, block, false);
614    }
615
616    __close(scopeManager) {
617        if (this.__shouldStaticallyClose(scopeManager)) {
618            return super.__close(scopeManager);
619        }
620
621        for (let i = 0, iz = this.__left.length; i < iz; ++i) {
622            const ref = this.__left[i];
623
624            ref.tainted = true;
625            this.__delegateToUpperScope(ref);
626        }
627        this.__left = null;
628
629        return this.upper;
630    }
631}
632
633class BlockScope extends Scope {
634    constructor(scopeManager, upperScope, block) {
635        super(scopeManager, "block", upperScope, block, false);
636    }
637}
638
639class SwitchScope extends Scope {
640    constructor(scopeManager, upperScope, block) {
641        super(scopeManager, "switch", upperScope, block, false);
642    }
643}
644
645class FunctionScope extends Scope {
646    constructor(scopeManager, upperScope, block, isMethodDefinition) {
647        super(scopeManager, "function", upperScope, block, isMethodDefinition);
648
649        // section 9.2.13, FunctionDeclarationInstantiation.
650        // NOTE Arrow functions never have an arguments objects.
651        if (this.block.type !== Syntax.ArrowFunctionExpression) {
652            this.__defineArguments();
653        }
654    }
655
656    isArgumentsMaterialized() {
657
658        // TODO(Constellation)
659        // We can more aggressive on this condition like this.
660        //
661        // function t() {
662        //     // arguments of t is always hidden.
663        //     function arguments() {
664        //     }
665        // }
666        if (this.block.type === Syntax.ArrowFunctionExpression) {
667            return false;
668        }
669
670        if (!this.isStatic()) {
671            return true;
672        }
673
674        const variable = this.set.get("arguments");
675
676        assert(variable, "Always have arguments variable.");
677        return variable.tainted || variable.references.length !== 0;
678    }
679
680    isThisMaterialized() {
681        if (!this.isStatic()) {
682            return true;
683        }
684        return this.thisFound;
685    }
686
687    __defineArguments() {
688        this.__defineGeneric(
689            "arguments",
690            this.set,
691            this.variables,
692            null,
693            null
694        );
695        this.taints.set("arguments", true);
696    }
697
698    // References in default parameters isn't resolved to variables which are in their function body.
699    //     const x = 1
700    //     function f(a = x) { // This `x` is resolved to the `x` in the outer scope.
701    //         const x = 2
702    //         console.log(a)
703    //     }
704    __isValidResolution(ref, variable) {
705
706        // If `options.nodejsScope` is true, `this.block` becomes a Program node.
707        if (this.block.type === "Program") {
708            return true;
709        }
710
711        const bodyStart = this.block.body.range[0];
712
713        // It's invalid resolution in the following case:
714        return !(
715            variable.scope === this &&
716            ref.identifier.range[0] < bodyStart && // the reference is in the parameter part.
717            variable.defs.every(d => d.name.range[0] >= bodyStart) // the variable is in the body.
718        );
719    }
720}
721
722class ForScope extends Scope {
723    constructor(scopeManager, upperScope, block) {
724        super(scopeManager, "for", upperScope, block, false);
725    }
726}
727
728class ClassScope extends Scope {
729    constructor(scopeManager, upperScope, block) {
730        super(scopeManager, "class", upperScope, block, false);
731    }
732}
733
734module.exports = {
735    Scope,
736    GlobalScope,
737    ModuleScope,
738    FunctionExpressionNameScope,
739    CatchScope,
740    WithScope,
741    BlockScope,
742    SwitchScope,
743    FunctionScope,
744    ForScope,
745    ClassScope
746};
747
748/* vim: set sw=4 ts=4 et tw=80 : */
749