diff --git a/jvm/src/test/scala/io/kaitai/struct/languages/PythonCompilerSpec.scala b/jvm/src/test/scala/io/kaitai/struct/languages/PythonCompilerSpec.scala new file mode 100644 index 000000000..d07d78aea --- /dev/null +++ b/jvm/src/test/scala/io/kaitai/struct/languages/PythonCompilerSpec.scala @@ -0,0 +1,83 @@ +package io.kaitai.struct.languages + +import io.kaitai.struct.format.{Identifier, NamedIdentifier, InstanceIdentifier} +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers._ + +class PythonCompilerSpec extends AnyFunSpec { + describe("PythonCompiler") { + describe("reserved keyword escaping") { + it("should escape Python reserved keyword 'class'") { + val id = NamedIdentifier("class") + val result = PythonCompiler.idToStr(id) + result should be("class_") + } + + it("should escape Python reserved keyword 'def'") { + val id = NamedIdentifier("def") + val result = PythonCompiler.idToStr(id) + result should be("def_") + } + + it("should escape Python reserved keyword 'if'") { + val id = NamedIdentifier("if") + val result = PythonCompiler.idToStr(id) + result should be("if_") + } + + it("should escape Python reserved keyword 'lambda'") { + val id = NamedIdentifier("lambda") + val result = PythonCompiler.idToStr(id) + result should be("lambda_") + } + + it("should escape Python reserved keyword 'return'") { + val id = NamedIdentifier("return") + val result = PythonCompiler.idToStr(id) + result should be("return_") + } + + it("should escape Python reserved keyword 'async'") { + val id = NamedIdentifier("async") + val result = PythonCompiler.idToStr(id) + result should be("async_") + } + + it("should escape Python reserved keyword 'await'") { + val id = NamedIdentifier("await") + val result = PythonCompiler.idToStr(id) + result should be("await_") + } + + it("should not escape non-reserved word 'class_name'") { + val id = NamedIdentifier("class_name") + val result = PythonCompiler.idToStr(id) + result should be("class_name") + } + + it("should not escape non-reserved word 'my_field'") { + val id = NamedIdentifier("my_field") + val result = PythonCompiler.idToStr(id) + result should be("my_field") + } + + it("should escape reserved keyword in InstanceIdentifier") { + val id = InstanceIdentifier("class") + val result = PythonCompiler.idToStr(id) + result should be("_m_class_") + } + + it("should handle privateMemberName with reserved keyword") { + val id = NamedIdentifier("class") + val result = PythonCompiler.privateMemberName(id) + result should be("self.class_") + } + + it("should handle privateMemberName with normal identifier") { + val id = NamedIdentifier("normal_field") + val result = PythonCompiler.privateMemberName(id) + result should be("self.normal_field") + } + } + } +} diff --git a/shared/src/main/scala/io/kaitai/struct/languages/PythonCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/PythonCompiler.scala index 152186f5b..205db9cb3 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/PythonCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/PythonCompiler.scala @@ -546,12 +546,30 @@ object PythonCompiler extends LanguageCompilerStatic config: RuntimeConfig ): LanguageCompiler = new PythonCompiler(tp, config) + // Python reserved keywords that need to be escaped + // https://docs.python.org/3/reference/lexical_analysis.html#keywords + val PYTHON_RESERVED_WORDS: Set[String] = Set( + "False", "None", "True", "and", "as", "assert", "async", "await", + "break", "class", "continue", "def", "del", "elif", "else", "except", + "finally", "for", "from", "global", "if", "import", "in", "is", + "lambda", "nonlocal", "not", "or", "pass", "raise", "return", + "try", "while", "with", "yield" + ) + + def escapePythonKeyword(name: String): String = { + if (PYTHON_RESERVED_WORDS.contains(name)) { + name + "_" + } else { + name + } + } + def idToStr(id: Identifier): String = id match { case SpecialIdentifier(name) => name - case NamedIdentifier(name) => name + case NamedIdentifier(name) => escapePythonKeyword(name) case NumberedIdentifier(idx) => s"_${NumberedIdentifier.TEMPLATE}$idx" - case InstanceIdentifier(name) => s"_m_$name" + case InstanceIdentifier(name) => s"_m_${escapePythonKeyword(name)}" case RawIdentifier(innerId) => s"_raw_${idToStr(innerId)}" }