// 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;
}
}