From ce1b5ffb08e7c8e7086294b9c9289ed738ef6b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Wed, 12 Feb 2025 12:53:30 +0100 Subject: [PATCH] fix: Introduce escaping some special chars in SCEX typed vars 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. --- .../scex/compiler/CodeGeneration.scala | 15 +++- .../scex/compiler/JavaScexCompilerTest.scala | 83 +++++++++++++++++-- 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/scex-core/src/main/scala/com/avsystem/scex/compiler/CodeGeneration.scala b/scex-core/src/main/scala/com/avsystem/scex/compiler/CodeGeneration.scala index 178386c..5aa1b58 100644 --- a/scex-core/src/main/scala/com/avsystem/scex/compiler/CodeGeneration.scala +++ b/scex-core/src/main/scala/com/avsystem/scex/compiler/CodeGeneration.scala @@ -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. @@ -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") diff --git a/scex-core/src/test/scala/com/avsystem/scex/compiler/JavaScexCompilerTest.scala b/scex-core/src/test/scala/com/avsystem/scex/compiler/JavaScexCompilerTest.scala index 1ffad05..f606a60 100644 --- a/scex-core/src/test/scala/com/avsystem/scex/compiler/JavaScexCompilerTest.scala +++ b/scex-core/src/test/scala/com/avsystem/scex/compiler/JavaScexCompilerTest.scala @@ -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]) @@ -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', '@', '/') }