JS: fix JsName renamer (KT-36484 fixed)

The following code was treated incorrectly:
```
function (_) {

  function foo() {
      try {} catch (_) {}
      try {} catch (_) {}
  }

  // _ is linked to the catch parameter here
}
```
This commit is contained in:
Anton Bannykh
2020-04-21 18:49:57 +03:00
parent 7d917634d0
commit de25d13f1a
3 changed files with 294 additions and 22 deletions
@@ -83,7 +83,9 @@ fun collectUsedNames(scope: JsNode): Set<JsName> {
return references
}
fun collectDefinedNames(scope: JsNode): Set<JsName> {
fun collectDefinedNames(scope: JsNode) = collectDefinedNames(scope, false)
fun collectDefinedNames(scope: JsNode, skipLabelsAndCatches: Boolean): Set<JsName> {
val names = mutableSetOf<JsName>()
object : RecursiveJsVisitor() {
@@ -107,12 +109,16 @@ fun collectDefinedNames(scope: JsNode): Set<JsName> {
}
override fun visitLabel(x: JsLabel) {
x.name?.let { names += it }
if (!skipLabelsAndCatches) {
x.name?.let { names += it }
}
super.visitLabel(x)
}
override fun visitCatch(x: JsCatch) {
names += x.parameter.name
if (!skipLabelsAndCatches) {
names += x.parameter.name
}
super.visitCatch(x)
}
@@ -158,7 +164,7 @@ fun collectDefinedNamesInAllScopes(scope: JsNode): Set<JsName> {
fun JsFunction.collectFreeVariables() = collectUsedNames(body) - collectDefinedNames(body) - parameters.map { it.name }
fun JsFunction.collectLocalVariables() = collectDefinedNames(body) + parameters.map { it.name }
fun JsFunction.collectLocalVariables(skipLabelsAndCatches: Boolean = false) = collectDefinedNames(body, skipLabelsAndCatches) + parameters.map { it.name }
fun collectNamedFunctions(scope: JsNode) = collectNamedFunctionsAndMetadata(scope).mapValues { it.value.first.function }
@@ -23,26 +23,30 @@ fun JsNode.fixForwardNameReferences() {
val currentScope = mutableMapOf<String, JsName>()
init {
currentScope += collectDefinedNames(this@fixForwardNameReferences).associateBy { it.ident }
currentScope += collectDefinedNames(this@fixForwardNameReferences, skipLabelsAndCatches = true).associateBy { it.ident }
}
private fun restore(ident: String, oldName: JsName?) {
if (oldName == null) {
currentScope -= ident
} else {
currentScope[ident] = oldName
}
}
override fun visitFunction(x: JsFunction) {
val scopeBackup = mutableMapOf<String, JsName?>()
val localVars = x.collectLocalVariables()
for (localVar in localVars) {
scopeBackup[localVar.ident] = currentScope[localVar.ident]
val localVars = x.collectLocalVariables(skipLabelsAndCatches = true).toList()
val backup = arrayOfNulls<JsName>(localVars.size)
localVars.forEachIndexed { index, localVar ->
backup[index] = currentScope[localVar.ident]
currentScope[localVar.ident] = localVar
}
super.visitFunction(x)
for ((ident, oldName) in scopeBackup) {
if (oldName == null) {
currentScope -= ident
}
else {
currentScope[ident] = oldName
}
for (index in localVars.indices.reversed()) {
restore(localVars[index].ident, backup[index])
}
}
@@ -53,12 +57,7 @@ fun JsNode.fixForwardNameReferences() {
super.visitCatch(x)
if (oldName != null) {
currentScope[name.ident] = name
}
else {
currentScope -= name.ident
}
restore(name.ident, oldName)
}
override fun visitNameRef(nameRef: JsNameRef) {
@@ -0,0 +1,267 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.js.test.ast
import com.google.gwt.dev.js.ThrowExceptionOnErrorReporter
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.inline.util.fixForwardNameReferences
import org.jetbrains.kotlin.js.parser.parse
import org.junit.Test
import kotlin.test.assertEquals
class FixForwardReferencesTest {
@Test
fun simple() {
"""
function foo() {
var a = 1;
return a;
}
""" parsesInto """
function foo_0() {
var a_0 = 1;
return a_0;
}
""" transformsInto """
function foo_0() {
var a_0 = 1;
return a_0;
}
"""
}
@Test
fun functionParameterClashesWithCatch() {
"""
function foo(a) {
try {} catch (a) {
a;
}
try {} catch (a) {
a;
}
return a;
}
""" parsesInto """
function foo_0(a_0) {
try {} catch (a_1) {
a_1;
}
try {} catch (a_2) {
a_2;
}
return a_0;
}
""" transformsInto """
function foo_0(a_0) {
try {} catch (a_1) {
a_1;
}
try {} catch (a_2) {
a_2;
}
return a_0;
}
"""
}
@Test
fun nestedCatchParameters() {
"""
try {} catch (a) {
a;
try {
a;
} catch (a) {
a;
}
a;
}
""" parsesInto """
try {} catch (a_0) {
a_0;
try {
a_0;
} catch (a_1) {
a_1;
}
a_0;
}
""" transformsInto """
try {} catch (a_0) {
a_0;
try {
a_0;
} catch (a_1) {
a_1;
}
a_0;
}
"""
}
@Test
fun labelsAndVarClash() {
"""
function () {
var a;
a: while (a) {
break a;
}
a;
}
""" parsesInto """
function () {
var a_0;
a_1: while (a_0) {
break a_1;
}
a_0;
}
""" transformsInto """
function () {
var a_0;
a_1: while (a_0) {
break a_1;
}
a_0;
}
"""
}
@Test fun catchParamAndVarClash() {
"""
{
var a;
try {} catch (a) { a }
a;
}
""" parsesInto """
{
var a_0;
try {} catch (a_1) { a_1 }
a_0;
}
""" transformsInto """
{
var a_0;
try {} catch (a_1) { a_1 }
a_0;
}
"""
}
@Test fun catchParamAndVarClashInsideFunction() {
"""
function () {
var a;
try {} catch (a) { a }
a;
}
""" parsesInto """
function () {
var a_0;
try {} catch (a_1) { a_1 }
a_0;
}
""" transformsInto """
function () {
var a_0;
try {} catch (a_1) { a_1 }
a_0;
}
"""
}
private infix fun String.parsesInto(result: String): String {
assertEquals(result.parse().toString(), parse().makeNamesUnique().toString(), "Unexpected parsing result")
return result
}
private infix fun String.transformsInto(result: String): String {
val node = toAst()
node.fixForwardNameReferences()
node.makeNamesUnique()
assertEquals(result.parse().toString(), node.toString(), "Unexpected transformation result")
return result
}
}
private fun String.parse(): JsNode = parse(this, ThrowExceptionOnErrorReporter, JsProgram().scope, "test")!!.single()
private fun JsNode.transformEachName(transform: JsName.() -> JsName): JsNode {
accept(object : RecursiveJsVisitor() {
override fun visitFunction(x: JsFunction) {
x.name?.let { x.name = it.transform() }
super.visitFunction(x)
}
override fun visitNameRef(nameRef: JsNameRef) {
nameRef.name?.let { nameRef.name = it.transform() }
super.visitNameRef(nameRef)
}
override fun visitParameter(x: JsParameter) {
x.name = x.name.transform()
super.visitParameter(x)
}
override fun visitLabel(x: JsLabel) {
x.name = x.name.transform()
super.visitLabel(x)
}
override fun visit(x: JsVars.JsVar) {
x.name = x.name.transform()
super.visit(x)
}
})
return this
}
private fun String.toAst(): JsNode {
val nameMapping = mutableMapOf<String, Pair<JsName, JsName>>()
return parse().transformEachName {
nameMapping[ident]?.let { (oldName, newName) ->
if (oldName != this) error("Duplicate unique name: ${ident}")
newName
} ?: run {
val parts = ident.split('_')
if (parts.size != 2) error("Unable to parse: $ident")
val (name, index) = parts
if (ident != "${name}_$index") error("Unable to parse: $ident")
JsName(name).also {
nameMapping[ident] = this to it
}
}
}
}
private fun JsNode.makeNamesUnique(): JsNode {
val nameCounter = mutableMapOf<String, Int>()
val nameMap = mutableMapOf<JsName, JsName>()
return transformEachName {
nameMap[this] ?: run {
val index = nameCounter.getOrDefault(ident, 0).also {
nameCounter[ident] = it + 1
}
JsName("${ident}_$index").also {
nameMap[this] = it
}
}
}
}