// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. package org.jetbrains.kotlin.js.backend.ast; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.js.util.Maps; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A scope is a factory for creating and allocating * {@link JsName}s. A JavaScript AST is * built in terms of abstract name objects without worrying about obfuscation, * keyword/identifier blacklisting, and so on. * <p/> * <p/> * <p/> * Scopes are associated with * {@link JsFunction}s, but the two are * not equivalent. Functions <i>have</i> scopes, but a scope does not * necessarily have an associated Function. Examples of this include the * {@link JsRootScope} and synthetic * scopes that might be created by a client. * <p/> * <p/> * <p/> * Scopes can have parents to provide constraints when allocating actual * identifiers for names. Specifically, names in child scopes are chosen such * that they do not conflict with names in their parent scopes. The ultimate * parent is usually the global scope (see * {@link JsProgram#getRootScope()}), * but parentless scopes are useful for managing names that are always accessed * with a qualifier and could therefore never be confused with the global scope * hierarchy. */ public abstract class JsScope { @NotNull private final String description; private Map<String, JsName> names = Collections.emptyMap(); private final JsScope parent; private static final Pattern FRESH_NAME_SUFFIX = Pattern.compile("[\\$_]\\d+$"); public JsScope(JsScope parent, @NotNull String description) { this.description = description; this.parent = parent; } protected JsScope(@NotNull String description) { this.description = description; parent = null; } @NotNull public JsScope innerObjectScope(@NotNull String scopeName) { return new JsObjectScope(this, scopeName); } /** * Gets a name object associated with the specified identifier in this scope, * creating it if necessary.<br/> * If the JsName does not exist yet, a new JsName is created. The identifier, * short name, and original name of the newly created JsName are equal to * the given identifier. * * @param identifier An identifier that is unique within this scope. */ @NotNull public JsName declareName(@NotNull String identifier) { JsName name = findOwnName(identifier); return name != null ? name : doCreateName(identifier); } /** * Creates a new variable with an unique ident in this scope. * The generated JsName is guaranteed to have an identifier that does not clash with any existing variables in the scope. * Future declarations of variables might however clash with the temporary * (unless they use this function). */ @NotNull public JsName declareFreshName(@NotNull String suggestedName) { assert !suggestedName.isEmpty(); String ident = getFreshIdent(suggestedName); return doCreateName(ident); } @NotNull public static JsName declareTemporaryName(@NotNull String suggestedName) { assert !suggestedName.isEmpty(); return new JsName(suggestedName, true); } /** * Creates a temporary variable with an unique name in this scope. * The generated temporary is guaranteed to have an identifier (but not short * name) that does not clash with any existing variables in the scope. * Future declarations of variables might however clash with the temporary. */ @NotNull public static JsName declareTemporary() { return declareTemporaryName("tmp$"); } /** * Attempts to find the name object for the specified ident, searching in this * scope, and if not found, in the parent scopes. * * @return <code>null</code> if the identifier has no associated name */ @Nullable public final JsName findName(@NotNull String ident) { JsName name = findOwnName(ident); if (name == null && parent != null) { return parent.findName(ident); } return name; } public boolean hasOwnName(@NotNull String name) { return names.containsKey(name); } private boolean hasName(@NotNull String name) { return hasOwnName(name) || (parent != null && parent.hasName(name)); } /** * Returns the parent scope of this scope, or <code>null</code> if this is the * root scope. */ public final JsScope getParent() { return parent; } public JsProgram getProgram() { assert (parent != null) : "Subclasses must override getProgram() if they do not set a parent"; return parent.getProgram(); } @Override public final String toString() { if (parent != null) { return description + "->" + parent; } else { return description; } } public void copyOwnNames(JsScope other) { names = new HashMap<>(names); names.putAll(other.names); } @NotNull public String getDescription() { return description; } @NotNull protected JsName doCreateName(@NotNull String ident) { JsName name = new JsName(ident, false); names = Maps.put(names, ident, name); return name; } /** * Attempts to find the name object for the specified ident, searching in this * scope only. * * @return <code>null</code> if the identifier has no associated name */ protected JsName findOwnName(@NotNull String ident) { return names.get(ident); } /** * During inlining names can be refreshed multiple times, * so "a" becomes "a_0", then becomes "a_0_0" * in case a_0 has been declared in calling scope. * * That's ugly. To resolve it, we rename * clashing names with "[_$]\\d+" suffix, * incrementing last number. * * Fresh name for "a0" should still be "a0_0". */ @NotNull protected String getFreshIdent(@NotNull String suggestedIdent) { char sep = '_'; String baseName = suggestedIdent; int counter = 0; Matcher matcher = FRESH_NAME_SUFFIX.matcher(suggestedIdent); if (matcher.find()) { String group = matcher.group(); baseName = matcher.replaceAll(""); sep = group.charAt(0); counter = Integer.valueOf(group.substring(1)); } String freshName = suggestedIdent; while (hasName(freshName)) { freshName = baseName + sep + counter++; } return freshName; } }