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        private 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            return doCreateName(ident);
098        }
099    
100        private String getNextTempName() {
101            // introduced by the compiler
102            return "tmp$" + (scopeId != null ? scopeId + "$" : "") + tempIndex++;
103        }
104    
105        /**
106         * Creates a temporary variable with an unique name in this scope.
107         * The generated temporary is guaranteed to have an identifier (but not short
108         * name) that does not clash with any existing variables in the scope.
109         * Future declarations of variables might however clash with the temporary.
110         */
111        @NotNull
112        public JsName declareTemporary() {
113            return declareFreshName(getNextTempName());
114        }
115    
116        /**
117         * Attempts to find the name object for the specified ident, searching in this
118         * scope, and if not found, in the parent scopes.
119         *
120         * @return <code>null</code> if the identifier has no associated name
121         */
122        @Nullable
123        public final JsName findName(@NotNull String ident) {
124            JsName name = findOwnName(ident);
125            if (name == null && parent != null) {
126                return parent.findName(ident);
127            }
128            return name;
129        }
130    
131        public boolean hasOwnName(@NotNull String name) {
132            return names.containsKey(name);
133        }
134    
135        @Nullable
136        public boolean hasName(@NotNull String name) {
137            return hasOwnName(name) || (parent != null && parent.hasName(name));
138        }
139    
140        /**
141         * Returns the parent scope of this scope, or <code>null</code> if this is the
142         * root scope.
143         */
144        public final JsScope getParent() {
145            return parent;
146        }
147    
148        public JsProgram getProgram() {
149            assert (parent != null) : "Subclasses must override getProgram() if they do not set a parent";
150            return parent.getProgram();
151        }
152    
153        @Override
154        public final String toString() {
155            if (parent != null) {
156                return description + "->" + parent;
157            }
158            else {
159                return description;
160            }
161        }
162    
163        public void copyOwnNames(JsScope other) {
164            names = new HashMap<String, JsName>(names);
165            names.putAll(other.names);
166        }
167    
168        @NotNull
169        public String getDescription() {
170            return description;
171        }
172    
173        @NotNull
174        protected JsName doCreateName(@NotNull String ident) {
175            JsName name = new JsName(this, ident);
176            names = Maps.put(names, ident, name);
177            return name;
178        }
179    
180        /**
181         * Attempts to find the name object for the specified ident, searching in this
182         * scope only.
183         *
184         * @return <code>null</code> if the identifier has no associated name
185         */
186        protected JsName findOwnName(@NotNull String ident) {
187            return names.get(ident);
188        }
189    
190        /**
191         * During inlining names can be refreshed multiple times,
192         * so "a" becomes "a_0", then becomes "a_0_0"
193         * in case a_0 has been declared in calling scope.
194         *
195         * That's ugly. To resolve it, we rename
196         * clashing names with "[_$]\\d+" suffix,
197         * incrementing last number.
198         *
199         * Fresh name for "a0" should still be "a0_0".
200         */
201        @NotNull
202        protected String getFreshIdent(@NotNull String suggestedIdent) {
203            char sep = '_';
204            String baseName = suggestedIdent;
205            int counter = 0;
206    
207            Matcher matcher = FRESH_NAME_SUFFIX.matcher(suggestedIdent);
208            if (matcher.find()) {
209                String group = matcher.group();
210                baseName = matcher.replaceAll("");
211                sep = group.charAt(0);
212                counter = Integer.valueOf(group.substring(1));
213            }
214    
215            String freshName = suggestedIdent;
216            while (hasName(freshName)) {
217                freshName = baseName + sep + counter++;
218            }
219    
220            return freshName;
221        }
222    }