Reject class-named members and unsupported constructor delegation
This commit is contained in:
1 parent
26a1ca9710
commit
04c9343a9d
5 files changed
+155
No files matched your search
@@ -133,6 +133,8 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
|||||||
- generic: `type Box<T> = List<T>`
|
- generic: `type Box<T> = List<T>`
|
||||||
- aliases are expanded structurally.
|
- aliases are expanded structurally.
|
||||||
- Classes/objects/enums/interfaces:
|
- Classes/objects/enums/interfaces:
|
||||||
|
- Constructors use the class name. A direct member function (including `static`) cannot have the same name as its enclosing class; local functions may still shadow names normally.
|
||||||
|
- `this(...)` and `this@Type(...)` are not secondary-constructor delegation syntax; call the class by name to construct another instance.
|
||||||
- `interface` is parsed as abstract class synonym.
|
- `interface` is parsed as abstract class synonym.
|
||||||
- `object` supports named singleton and anonymous object expression forms.
|
- `object` supports named singleton and anonymous object expression forms.
|
||||||
- enums support lifted entries: `enum E* { A, B }`.
|
- enums support lifted entries: `enum E* { A, B }`.
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
### Ticket 1 — Name resolution: a member function named like its class silently shadows the primary constructor inside the class body
|
||||||
|
|
||||||
|
**Summary.** `class` bodies accept a member function whose name equals the class name. That member then wins name resolution for **all unqualified calls inside the class body**, including `init` blocks, method bodies and nested lambdas. The primary constructor becomes unreachable from inside the class, with no escape hatch. Compiles cleanly, breaks at runtime.
|
||||||
|
|
||||||
|
**Repro**
|
||||||
|
```lyng
|
||||||
|
class C1(x) {
|
||||||
|
fun C1(y): String = "member " + y.toString()
|
||||||
|
init { println(C1(9)) } // -> "member C1(9)", NOT the constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
class C2(x) {
|
||||||
|
fun C2(y, z): String = y.toString() + z.toString()
|
||||||
|
fun probe(): String = C2(7, 8) // member chosen although ctor takes 1 arg
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fact(x) {
|
||||||
|
fun Fact(y): Fact = Fact(y * 2) // compiles; unqualified Fact(...) = itself
|
||||||
|
}
|
||||||
|
Fact(1).Fact(3) // java.lang.StackOverflowError
|
||||||
|
```
|
||||||
|
Local functions shadow too:
|
||||||
|
```lyng
|
||||||
|
class M3(x) {
|
||||||
|
fun probe(y): String {
|
||||||
|
fun M3(z): String = "local " + z.toString()
|
||||||
|
return M3(y) // local member wins, ctor unreachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
No workaround found:
|
||||||
|
```lyng
|
||||||
|
class Q1(x) {
|
||||||
|
fun Q1(y): String = "member"
|
||||||
|
fun a(): String = Q1.Q1(3) // IllegalArgumentException: expected 1 arguments, got 0
|
||||||
|
fun b(): String = (Q1(5) as Q1).Q1(6) // ClassCastException: Cannot cast String to Q1
|
||||||
|
}
|
||||||
|
class G1(x) { fun g(): G1 = this(x + 1) } // Exception: operation is not implemented
|
||||||
|
```
|
||||||
|
Related symptom: adding `Type::class` in a class that also has a same-name member turns the **declaration** itself into a hard error pointing at the wrong place:
|
||||||
|
```lyng
|
||||||
|
class D16(x) {
|
||||||
|
fun D16(y): String = "member " + y.toString() // Error: expected 1 arguments, got 0
|
||||||
|
fun lit(): String = D16::class.toString()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Control cases compile fine: same-name member alone ✔, `::class` alone ✔, the combination ✘.
|
||||||
|
|
||||||
|
**Expected.** Either (a) reject a member whose name collides with the class name at compile time (preferred — it is never a constructor in Lyng, there are no secondary constructors), or (b) keep constructor calls resolvable for the primary arity and fall back to members only for non-matching arity, plus a working `this(...)`/`this@Type(...)` delegation. In any case `Type::class` must not be affected by a same-name member, and diagnostics must point at the actual construct.
|
||||||
|
|
||||||
|
**Suggested tests.** `fun SameName(...)` declared in a class with the same name → expected compile error; self-recursive factory → must not reach runtime stack overflow; `Type::class` inside such a class → must compile; `this(...)` → either implement or emit a clear compile-time error instead of `operation is not implemented`.
|
||||||
|
|
||||||
|
|
||||||
|
## Resolution
|
||||||
|
|
||||||
|
Valid: direct instance/static member functions named after their enclosing class
|
||||||
|
are now rejected at the function name. This also rejects the collision before
|
||||||
|
any `::class` expression can produce a misleading downstream diagnostic.
|
||||||
|
The supported control is `Type::class` in a class without the illegal member;
|
||||||
|
the suggested test asking the illegal declaration to compile contradicts the
|
||||||
|
report's preferred resolution.
|
||||||
|
|
||||||
|
Direct `this(...)` / `this@Type(...)` calls on the enclosing class instance now
|
||||||
|
report unsupported constructor delegation during compilation. Use `Type(...)`
|
||||||
|
to construct another instance; no secondary constructors were introduced.
|
||||||
|
|
||||||
|
Local functions shadowing class names are ordinary lexical shadowing, not a bug,
|
||||||
|
and remain supported. Regression coverage: `ClassNameMemberRegressionTest`.
|
||||||
|
Before the fix, 2 of its 4 tests failed; the constructor/class-literal and local
|
||||||
|
shadowing controls passed.
|
||||||
|
|
||||||
|
Validation: all 4 focused JVM regression tests passed, followed by the full
|
||||||
|
`:lynglib:jvmTest` suite (benchmarks excluded by the repository default).
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Invalid reports
|
||||||
|
|
||||||
|
Place reports here when reproduction and the language contract show that they
|
||||||
|
are not bugs. Append the reason and verification evidence to each moved report.
|
||||||
|
Valid reports with an invalid subclaim stay with their resolution notes.
|
||||||
@@ -7458,6 +7458,14 @@ class Compiler(
|
|||||||
isOptional: Boolean,
|
isOptional: Boolean,
|
||||||
explicitTypeArgs: List<TypeDecl>? = null
|
explicitTypeArgs: List<TypeDecl>? = null
|
||||||
): ObjRef {
|
): ObjRef {
|
||||||
|
val thisTypeName = when (left) {
|
||||||
|
is LocalVarRef -> if (left.name == "this") currentImplicitThisTypeName() else null
|
||||||
|
is QualifiedThisRef -> left.typeName
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (thisTypeName != null && codeContexts.any { it is CodeContext.ClassBody && it.name == thisTypeName }) {
|
||||||
|
throw ScriptError(cc.currentPos(), "constructor delegation through this is not supported; call the class by name")
|
||||||
|
}
|
||||||
var detectedBlockArgument = blockArgument
|
var detectedBlockArgument = blockArgument
|
||||||
val expectedReceiver = tailBlockReceiverType(left)
|
val expectedReceiver = tailBlockReceiverType(left)
|
||||||
val expectedTailCallable = expectedCallableArgumentType(left, 0)
|
val expectedTailCallable = expectedCallableArgumentType(left, 0)
|
||||||
@@ -9984,6 +9992,10 @@ class Compiler(
|
|||||||
name = t.value
|
name = t.value
|
||||||
nameStartPos = t.pos
|
nameStartPos = t.pos
|
||||||
}
|
}
|
||||||
|
if (parentContext is CodeContext.ClassBody && extTypeName == null &&
|
||||||
|
name == parentContext.name.substringAfterLast('.')) {
|
||||||
|
throw ScriptError(nameStartPos, "member function cannot have the same name as its enclosing class: $name")
|
||||||
|
}
|
||||||
val extensionWrapperName = extTypeName?.let { extensionCallableName(it, name) }
|
val extensionWrapperName = extTypeName?.let { extensionCallableName(it, name) }
|
||||||
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
|
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
|
||||||
var memberMethodId = if (extTypeName == null) classCtx?.memberMethodIds?.get(name) else null
|
var memberMethodId = if (extTypeName == null) classCtx?.memberMethodIds?.get(name) else null
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.ScriptError
|
||||||
|
import net.sergeych.lyng.eval
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertContains
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class ClassNameMemberRegressionTest {
|
||||||
|
@Test
|
||||||
|
fun rejectsClassNamedMembersAtTheirDeclaration() = runTest {
|
||||||
|
for (member in listOf(
|
||||||
|
"fun Same(y): String = y.toString()",
|
||||||
|
"fun Same(y, z): String = y.toString() + z.toString()",
|
||||||
|
"fun Same(y): Same = Same(y * 2)",
|
||||||
|
"static fun Same(y): Same = Same(y)",
|
||||||
|
"fun Same(y): String = \"member\"; fun literal() = Same::class.toString()"
|
||||||
|
)) {
|
||||||
|
val error = assertFailsWith<ScriptError> {
|
||||||
|
eval("class Same(x) {\n $member\n}")
|
||||||
|
}
|
||||||
|
assertContains(error.errorMessage, "member function cannot have the same name as its enclosing class")
|
||||||
|
assertEquals(1, error.pos.line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun thisIsNotConstructorDelegation() = runTest {
|
||||||
|
for (receiver in listOf("this", "this@Factory")) {
|
||||||
|
val error = assertFailsWith<ScriptError> {
|
||||||
|
eval("class Factory(x) { fun again(): Factory = $receiver(x + 1) }")
|
||||||
|
}
|
||||||
|
assertContains(error.errorMessage, "constructor delegation through this is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun constructorAndClassLiteralRemainAvailable() = runTest {
|
||||||
|
assertEquals("3", eval("""
|
||||||
|
class Factory(val x: Int) {
|
||||||
|
fun again(y: Int): Factory = Factory(y)
|
||||||
|
fun literal(): String = Factory::class.toString()
|
||||||
|
}
|
||||||
|
val f = Factory(1)
|
||||||
|
f.literal()
|
||||||
|
f.again(3).x.toString()
|
||||||
|
""".trimIndent()).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun localFunctionsCanStillShadowClassNames() = runTest {
|
||||||
|
assertEquals("local 7", eval("""
|
||||||
|
class Local(x) {
|
||||||
|
fun probe(y): String {
|
||||||
|
fun Local(z): String = "local " + z.toString()
|
||||||
|
return Local(y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Local(1).probe(7)
|
||||||
|
""".trimIndent()).toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user