001    // Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file
002    // for details. All rights reserved. Use of this source code is governed by a
003    // BSD-style license that can be found in the LICENSE file.
004    
005    package com.google.dart.compiler.backend.js.ast;
006    
007    import com.google.dart.compiler.util.Maps;
008    import org.jetbrains.annotations.NotNull;
009    import org.jetbrains.annotations.Nullable;
010    
011    import java.util.Collections;
012    import java.util.HashMap;
013    import java.util.Map;
014    import java.util.regex.Matcher;
015    import java.util.regex.Pattern;
016    
017    import static com.google.dart.compiler.backend.js.ast.JsScopesKt.JsObjectScope;
018    
019    /**
020     * A scope is a factory for creating and allocating
021     * {@link JsName}s. A JavaScript AST is
022     * built in terms of abstract name objects without worrying about obfuscation,
023     * keyword/identifier blacklisting, and so on.
024     * <p/>
025     * <p/>
026     * <p/>
027     * Scopes are associated with
028     * {@link JsFunction}s, but the two are
029     * not equivalent. Functions <i>have</i> scopes, but a scope does not
030     * necessarily have an associated Function. Examples of this include the
031     * {@link JsRootScope} and synthetic
032     * scopes that might be created by a client.
033     * <p/>
034     * <p/>
035     * <p/>
036     * Scopes can have parents to provide constraints when allocating actual
037     * identifiers for names. Specifically, names in child scopes are chosen such
038     * that they do not conflict with names in their parent scopes. The ultimate
039     * parent is usually the global scope (see
040     * {@link JsProgram#getRootScope()}),
041     * but parentless scopes are useful for managing names that are always accessed
042     * with a qualifier and could therefore never be confused with the global scope
043     * hierarchy.
044     */
045    public abstract class JsScope {
046        @NotNull
047        private final String description;
048        private Map<String, JsName> names = Collections.emptyMap();
049        private final JsScope parent;
050        protected int tempIndex = 0;
051        private final String scopeId;
052    
053        private static final Pattern FRESH_NAME_SUFFIX = Pattern.compile("[\\$_]\\d+$");
054    
055        public JsScope(JsScope parent, @NotNull String description, @Nullable String scopeId) {
056            this.scopeId = scopeId;
057            this.description = description;
058            this.parent = parent;
059        }
060    
061        protected JsScope(@NotNull String description) {
062            this.description = description;
063            parent = null;
064            scopeId = null;
065        }
066    
067        @NotNull
068        public JsScope innerObjectScope(@NotNull String scopeName) {
069            return JsObjectScope(this, scopeName);
070        }
071    
072        /**
073         * Gets a name object associated with the specified identifier in this scope,
074         * creating it if necessary.<br/>
075         * If the JsName does not exist yet, a new JsName is created. The identifier,
076         * short name, and original name of the newly created JsName are equal to
077         * the given identifier.
078         *
079         * @param identifier An identifier that is unique within this scope.
080         */
081        @NotNull
082        public JsName declareName(@NotNull String identifier) {
083            JsName name = findOwnName(identifier);
084            return name != null ? name : doCreateName(identifier);
085        }
086    
087        /**
088         * Creates a new variable with an unique ident in this scope.
089         * The generated JsName is guaranteed to have an identifier that does not clash with any existing variables in the scope.
090         * Future declarations of variables might however clash with the temporary
091         * (unless they use this function).
092         */
093        @NotNull
094        public JsName declareFreshName(@NotNull String suggestedName) {
095            assert !suggestedName.isEmpty();
096            String ident = getFreshIdent(suggestedName);
097            assert !hasOwnName(ident);
098            return doCreateName(ident);
099        }
100    
101        private String getNextTempName() {
102            // introduced by the compiler
103            return "tmp$" + (scopeId != null ? scopeId + "$" : "") + tempIndex++;
104        }
105    
106        /**
107         * Creates a temporary variable with an unique name in this scope.
108         * The generated temporary is guaranteed to have an identifier (but not short
109         * name) that does not clash with any existing variables in the scope.
110         * Future declarations of variables might however clash with the temporary.
111         */
112        @NotNull
113        public JsName declareTemporary() {
114            return declareFreshName(getNextTempName());
115        }
116    
117        /**
118         * Attempts to find the name object for the specified ident, searching in this
119         * scope, and if not found, in the parent scopes.
120         *
121         * @return <code>null</code> if the identifier has no associated name
122         */
123        @Nullable
124        public final JsName findName(String ident) {
125            JsName name = findOwnName(ident);
126            if (name == null && parent != null) {
127                return parent.findName(ident);
128            }
129            return name;
130        }
131    
132        public boolean hasOwnName(@NotNull String name) {
133            return names.containsKey(name);
134        }
135    
136        /**
137         * Returns the parent scope of this scope, or <code>null</code> if this is the
138         * root scope.
139         */
140        public final JsScope getParent() {
141            return parent;
142        }
143    
144        public JsProgram getProgram() {
145            assert (parent != null) : "Subclasses must override getProgram() if they do not set a parent";
146            return parent.getProgram();
147        }
148    
149        @Override
150        public final String toString() {
151            if (parent != null) {
152                return description + "->" + parent;
153            }
154            else {
155                return description;
156            }
157        }
158    
159        public void copyOwnNames(JsScope other) {
160            names = new HashMap<String, JsName>(names);
161            names.putAll(other.names);
162        }
163    
164        @NotNull
165        public String getDescription() {
166            return description;
167        }
168    
169        @NotNull
170        protected JsName doCreateName(@NotNull String ident) {
171            JsName name = new JsName(this, ident);
172            names = Maps.put(names, ident, name);
173            return name;
174        }
175    
176        /**
177         * Attempts to find the name object for the specified ident, searching in this
178         * scope only.
179         *
180         * @return <code>null</code> if the identifier has no associated name
181         */
182        protected JsName findOwnName(@NotNull String ident) {
183            return names.get(ident);
184        }
185    
186        /**
187         * During inlining names can be refreshed multiple times,
188         * so "a" becomes "a_0", then becomes "a_0_0"
189         * in case a_0 has been declared in calling scope.
190         *
191         * That's ugly. To resolve it, we rename
192         * clashing names with "[_$]\\d+" suffix,
193         * incrementing last number.
194         *
195         * Fresh name for "a0" should still be "a0_0".
196         */
197        @NotNull
198        protected String getFreshIdent(@NotNull String suggestedIdent) {
199            char sep = '_';
200            String baseName = suggestedIdent;
201            int counter = 0;
202    
203            Matcher matcher = FRESH_NAME_SUFFIX.matcher(suggestedIdent);
204            if (matcher.find()) {
205                String group = matcher.group();
206                baseName = matcher.replaceAll("");
207                sep = group.charAt(0);
208                counter = Integer.valueOf(group.substring(1));
209            }
210    
211            String freshName = suggestedIdent;
212            while (hasOwnName(freshName)) {
213                freshName = baseName + sep + counter++;
214            }
215    
216            return freshName;
217        }
218    }