Skip to content

Commit

Permalink
fix: Introduce escaping some special chars in SCEX typed vars
Browse files Browse the repository at this point in the history
Previously, commonly used chars like space, @, ", would cause SCEX compilation errors if used in typed var names. These errors happened due to inconsistency in how typed variables and their types are defined in CodeGeneration macros.

There are still some special chars like \n or ` that will break the macros generating the code for typed vars, but they might be less of a problem if they are publicly announced via TypedVariables.IllegalCharsInVarName. Without such public declaration the responsibility for validating the names is shifted towards the end application, while it isn't the one to impose the restrictions and throw compilation error.
  • Loading branch information
Michał Grabowski committed Feb 12, 2025
1 parent a92fcf3 commit ce1b5ff
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ object CodeGeneration {
def escapeString(str: String) =
str.flatMap(escapedChar)

object TypedVariables {
/**
* Use this to validate variable names in your code - vars with these characters in names are guaranteed to fail
* due to compilation error.
*/
val IllegalCharsInVarName: Set[Char] = Set('\n', '\r', '`', '\\')
}

/**
* Generates code of implicit view for given Java class that adds Scala-style getters
* forwarding to existing Java-style getters of given class.
Expand Down Expand Up @@ -178,14 +186,15 @@ object CodeGeneration {

val typedVariables = variableTypes.toList.sorted.iterator.map {
case (name, tpe) =>
val escapedName = escapeString(name)
s"""
| private def ${name}VarTag = ${validatePrefix}inferVarTag[$tpe]$validatePostfix
| private def `${escapedName}VarTag` = ${validatePrefix}inferVarTag[$tpe]$validatePostfix
|
| @$AnnotationPkg.NotValidated def `$name`: $tpe =
| $ContextSymbol.getTypedVariable("${escapeString(name)}")(${name}VarTag)
| $ContextSymbol.getTypedVariable("$escapedName")(`${escapedName}VarTag`)
|
| @$AnnotationPkg.NotValidated def `${name}_=`(value: $tpe): Unit =
| $ContextSymbol.setTypedVariable("${escapeString(name)}", value)(${name}VarTag)
| $ContextSymbol.setTypedVariable("$escapedName", value)(`${escapedName}VarTag`)
""".stripMargin
}.mkString("\n")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
package com.avsystem.scex.compiler

import com.avsystem.scex.compiler.CodeGeneration.{TypedVariables, escapedChar}
import com.avsystem.scex.compiler.JavaScexCompilerTest.SuspiciousCharsAllowedInVariableNames
import com.avsystem.scex.compiler.ScexCompiler.CompilationFailedException
import com.avsystem.scex.japi.{ScalaTypeTokens, XmlFriendlyJavaScexCompiler}
import com.avsystem.scex.util.{PredefinedAccessSpecs, SimpleContext}
import org.scalatest.funsuite.AnyFunSuite

/**
* Author: ghik
* Created: 26/10/15.
*/
* Author: ghik
* Created: 26/10/15.
*/
class JavaScexCompilerTest extends AnyFunSuite with CompilationTest {
override protected def createCompiler = new XmlFriendlyJavaScexCompiler(new ScexSettings)

test("typed variables test") {
// given a context with a valid typed variable
val acl = PredefinedAccessSpecs.basicOperations
val expr = "#someDouble.toDegrees"
val context = SimpleContext(())
context.setTypedVariable("someDouble", math.Pi)

// given expression that uses the typed variable
val expr = "#someDouble.toDegrees"

// then expression is compiled without errors
val cexpr = compiler.buildExpression
.contextType(ScalaTypeTokens.create[SimpleContext[Unit]])
.resultType(classOf[Double])
Expand All @@ -26,6 +33,72 @@ class JavaScexCompilerTest extends AnyFunSuite with CompilationTest {
.profile(createProfile(acl))
.get

assert(180.0 == cexpr(context))
// when expression is evaluated
val result = cexpr(context)

// then the result is correct
assert(math.Pi.toDegrees == result)
}

TypedVariables.IllegalCharsInVarName.foreach { char =>
test(s"typed variables fail if char [code=${char.toInt}, escaped=${escapedChar(char)}] is in var name test") {
// given a context with a typed variable with illegal char in name
val acl = PredefinedAccessSpecs.basicOperations
val variableName = s"JP2NadajePoRazPierwszy$char"
val context = SimpleContext(())
context.setTypedVariable(variableName, math.Pi)

// given expression that uses the typed variable
val expr = s"#`$variableName`.toDegrees"

// then compilation should fail
withClue(s"[code=${char.toInt}] character in variable name [$variableName] should not be allowed") {
assertThrows[CompilationFailedException] {
compiler.buildExpression
.contextType(ScalaTypeTokens.create[SimpleContext[Unit]])
.resultType(classOf[Double])
.expression(expr)
.template(false)
.variableClass(variableName, classOf[Double])
.profile(createProfile(acl))
.get
}
}
}
}

SuspiciousCharsAllowedInVariableNames.foreach { char =>
test(s"typed variables work if char [code=$char.toInt] is in var name test") {
// given a context with a typed variable with non-standard characters in name
val acl = PredefinedAccessSpecs.basicOperations
val variableName = s"""2JPZnowuNadaje$char"""
val context = SimpleContext(())
context.setTypedVariable(variableName, math.Pi)

// given expression that uses the typed variable
val expr = s"#`$variableName`.toDegrees"

// then expression is compiled without errors
val cexpr = compiler.buildExpression
.contextType(ScalaTypeTokens.create[SimpleContext[Unit]])
.resultType(classOf[Double])
.expression(expr)
.template(false)
.variableClass(variableName, classOf[Double])
.profile(createProfile(acl))
.get

// when expression is evaluated
val result = cexpr(context)

// then the result is correct
assert(math.Pi.toDegrees == result)
}
}

}

object JavaScexCompilerTest {
// These characters caused compilation errors if they were put in variable names until v1.35 release
private val SuspiciousCharsAllowedInVariableNames: Seq[Char] = Seq(' ', '\t', '@', '/')
}

0 comments on commit ce1b5ff

Please sign in to comment.