diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ecffaf..33d06dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ### Unreleased +- Language: Refined `protected` visibility rules + - Ancestor classes can now access `protected` members of their descendants, provided the ancestor also defines or inherits a member with the same name (indicating an override of a member known to the ancestor). + - This allows patterns where a base class calls a `protected` method that is implemented in a subclass. + - Fixed a regression where self-calls to unmangled members sometimes bypassed visibility checks incorrectly; these are now handled by the refined logic. + - Language: Added `return` statement - `return [expression]` exits the innermost enclosing callable (function or lambda). - Supports non-local returns using `@label` syntax (e.g., `return@outer 42`). @@ -96,7 +101,7 @@ - Header-specified constructor arguments are passed to direct bases. - Visibility enforcement under MI: - `private` visible only inside the declaring class body. - - `protected` visible inside the declaring class and any of its transitive subclasses; unrelated contexts cannot access it (qualification/casts do not bypass). + - `protected` visible inside the declaring class and its transitive subclasses. Additionally, ancestor classes can access protected members of their descendants if it's an override of a member known to the ancestor. Unrelated contexts cannot access it (qualification/casts do not bypass). - Diagnostics improvements: - Missing member/field messages include receiver class and linearization order; hints for `this@Type` or casts when helpful. - Invalid `this@Type` reports that the qualifier is not an ancestor and shows the receiver lineage. diff --git a/LYNG_AI_SPEC.md b/LYNG_AI_SPEC.md index 99954a8..91528a3 100644 --- a/LYNG_AI_SPEC.md +++ b/LYNG_AI_SPEC.md @@ -32,7 +32,7 @@ High-density specification for LLMs. Reference this for all Lyng code generation val area get = π * r * r ``` - **Mandatory `override`**: Required for all members existing in the ancestor chain. -- **Visibility**: `public` (default), `protected` (subclasses), `private` (this class instance only). `private set` / `protected set` allowed on properties. +- **Visibility**: `public` (default), `protected` (subclasses and ancestors for overrides), `private` (this class instance only). `private set` / `protected set` allowed on properties. - **Disambiguation**: `this@Base.member()` or `(obj as Base).member()`. `as` returns a qualified view. - **Abstract/Interface**: `interface` is a synonym for `abstract class`. Both support state and constructors. - **Extensions**: `fun Class.ext()` or `val Class.ext get = ...`. Scope-isolated. diff --git a/docs/OOP.md b/docs/OOP.md index eecf958..76cc8d8 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -425,7 +425,7 @@ Key rules and features: - Visibility - `private`: accessible only inside the declaring class body; not visible in subclasses and cannot be accessed via `this@Type` or casts. - - `protected`: accessible in the declaring class and in any of its transitive subclasses (including MI), but not from unrelated contexts; qualification/casts do not bypass it. + - `protected`: accessible in the declaring class and in any of its transitive subclasses (including MI). Additionally, ancestor classes can access protected members of their descendants if it's an override of a member known to the ancestor. Protected members are not visible from unrelated contexts; qualification/casts do not bypass it. ## Abstract Classes and Members @@ -859,21 +859,29 @@ Private fields are visible only _inside the class instance_: ### Protected members -Protected members are available to the declaring class and all of its transitive subclasses (including via MI), but not from unrelated contexts: +Protected members are available to the declaring class and all of its transitive subclasses (including via MI). Additionally, an ancestor class can access a `protected` member of its descendant if the ancestor also defines or inherits a member with the same name (i.e., it is an override of something the ancestor knows about). -``` -class A() { - protected fun ping() { "pong" } -} -class B() : A() { - fun call() { this@A.ping() } +Protected members are not available from unrelated contexts: + +```lyng +class Base { + abstract protected fun foo() + + fun bar() { + // Ancestor can see foo() because it's an override + // of a member it defines (even as abstract): + foo() + } } -val b = B() -assertEquals("pong", b.call()) +class Derived : Base { + override protected fun foo() { "ok" } +} + +assertEquals("ok", Derived().bar()) // Unrelated access is forbidden, even via cast -assertThrows { (b as A).ping() } +assertThrows { (Derived() as Base).foo() } ``` It is possible to provide private constructor parameters so they can be diff --git a/docs/tutorial.md b/docs/tutorial.md index 3696788..2badb6b 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1701,7 +1701,7 @@ assertEquals(null, (buzz as? Foo)?.runA()) Notes: - Resolution order uses C3 MRO (active): deterministic, monotonic order suitable for diamonds and complex hierarchies. Example: for `class D() : B(), C()` where both `B()` and `C()` derive from `A()`, the C3 order is `D → B → C → A`. The first visible match wins. -- `private` is visible only inside the declaring class; `protected` is visible from the declaring class and any of its transitive subclasses. Qualialsofication (`this@Type`) or casts do not bypass visibility. +- `private` is visible only inside the declaring class; `protected` is visible from the declaring class and its subclasses. Additionally, ancestors can access protected members of descendants if they override a member known to the ancestor. Qualification (`this@Type`) or casts do not bypass visibility. - Safe‑call `?.` works with `as?` for optional dispatch. ## Extension members diff --git a/docs/whats_new.md b/docs/whats_new.md index 0a118b5..37bf7b8 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -24,6 +24,9 @@ class Person(private var _age: Int) { ### Private and Protected Setters You can now restrict the visibility of a property's or field's setter using `private set` or `protected set`. This allows members to be publicly readable but only writable from within the declaring class or its subclasses. +### Refined Protected Visibility +Ancestor classes can now access `protected` members of their descendants if it is an override of a member known to the ancestor. This enables base classes to call protected methods that are implemented or overridden in subclasses. + ```lyng class Counter { var count = 0 diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index b33d6e2..e4431c9 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "1.2.0" +version = "1.2.1-SNAPSHOT" // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt index 159f0cb..ff891b0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt @@ -54,14 +54,14 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) : for (cls in effectiveClass.mro) { val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) if (rec != null && !rec.isAbstract) { - if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) { + if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx, name)) { return rec.copy(receiver = receiver) } } } // Finally, root object fallback Obj.rootObjectType.members[name]?.let { rec -> - if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) { + if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx, name)) { return rec.copy(receiver = receiver) } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 083530f..cfc180f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -112,7 +112,7 @@ open class Scope( } } s.objects[name]?.let { rec -> - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec + if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec } caller?.let { ctx -> s.localBindings[ctx.mangledName(name)]?.let { rec -> @@ -120,11 +120,11 @@ open class Scope( } } s.localBindings[name]?.let { rec -> - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec + if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec } s.getSlotIndexOf(name)?.let { idx -> val rec = s.getSlotRecord(idx) - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec + if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec } return null } @@ -163,7 +163,7 @@ open class Scope( this.extensions[cls]?.get(name)?.let { return it } } return thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec -> - if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) { + if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx, name)) { if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null else rec } else null @@ -186,7 +186,7 @@ open class Scope( s.extensions[cls]?.get(name)?.let { return it } } s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec -> - if (canAccessMember(rec.visibility, rec.declaringClass, caller)) { + if (canAccessMember(rec.visibility, rec.declaringClass, caller, name)) { if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) { // ignore fields, properties and abstracts here, they will be handled by the caller via readField } else return rec @@ -358,14 +358,14 @@ open class Scope( for (cls in effectiveClass.mro) { val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) if (rec != null && !rec.isAbstract) { - if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) { + if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx, name)) { return rec.copy(receiver = receiver) } } } // Finally, root object fallback Obj.rootObjectType.members[name]?.let { rec -> - if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) { + if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx, name)) { return rec.copy(receiver = receiver) } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Visibility.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Visibility.kt index d0af831..ba00039 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Visibility.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Visibility.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,12 @@ enum class Visibility { } /** MI-aware visibility check: whether [caller] can access a member declared in [decl] with [visibility]. */ -fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass?, caller: net.sergeych.lyng.obj.ObjClass?): Boolean { +fun canAccessMember( + visibility: Visibility, + decl: net.sergeych.lyng.obj.ObjClass?, + caller: net.sergeych.lyng.obj.ObjClass?, + name: String? = null +): Boolean { val res = when (visibility) { Visibility.Public -> true Visibility.Private -> (decl != null && caller === decl) @@ -33,7 +38,14 @@ fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass decl == null -> false caller == null -> false caller === decl -> true - else -> (caller.allParentsSet.contains(decl)) + caller.allParentsSet.contains(decl) -> true + name != null && decl.allParentsSet.contains(caller) -> { + // Ancestor can access protected member of its descendant if it also has this member + // (i.e. it's an override of something the ancestor knows about) + val existing = caller.getInstanceMemberOrNull(name, includeAbstract = true) + existing != null && (existing.visibility == Visibility.Protected || existing.visibility == Visibility.Public) + } + else -> false } } return res diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index ffd3d9b..c7d68ac 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -113,7 +113,7 @@ open class Obj { if (rec != null && !rec.isAbstract) { val decl = rec.declaringClass ?: cls val caller = scope.currentClassCtx - if (!canAccessMember(rec.visibility, decl, caller)) + if (!canAccessMember(rec.visibility, decl, caller, name)) scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})")) if (rec.type == ObjRecord.Type.Property) { @@ -140,7 +140,7 @@ open class Obj { cls.members[name]?.let { rec -> val decl = rec.declaringClass ?: cls val caller = scope.currentClassCtx - if (!canAccessMember(rec.visibility, decl, caller)) + if (!canAccessMember(rec.visibility, decl, caller, name)) scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})")) if (rec.type == ObjRecord.Type.Property) { @@ -482,7 +482,7 @@ open class Obj { cls.members[name]?.let { rec -> val decl = rec.declaringClass ?: cls val caller = scope.currentClassCtx - if (!canAccessMember(rec.visibility, decl, caller)) + if (!canAccessMember(rec.visibility, decl, caller, name)) scope.raiseError(ObjIllegalAccessException(scope, "can't access field ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})")) val resolved = resolveRecord(scope, rec, name, decl) if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement) @@ -526,7 +526,7 @@ open class Obj { } val caller = scope.currentClassCtx // Check visibility for non-property members here if they weren't checked before - if (!canAccessMember(obj.visibility, decl, caller)) + if (!canAccessMember(obj.visibility, decl, caller, name)) scope.raiseError(ObjIllegalAccessException(scope, "can't access field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})")) return obj } @@ -574,7 +574,7 @@ open class Obj { val decl = field.declaringClass val caller = scope.currentClassCtx - if (!canAccessMember(field.effectiveWriteVisibility, decl, caller)) + if (!canAccessMember(field.effectiveWriteVisibility, decl, caller, name)) scope.raiseError(ObjIllegalAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})")) if (field.type == ObjRecord.Type.Delegated) { val del = field.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate") diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index ed491c9..9ee710b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -461,7 +461,7 @@ open class ObjClass( if (existing != null && existing.declaringClass != this) { // If the existing member is private in the ancestor, it's not visible for overriding. // It should be treated as a new member in this class. - if (!existing.visibility.isPublic && !canAccessMember(existing.visibility, existing.declaringClass, this)) { + if (!existing.visibility.isPublic && !canAccessMember(existing.visibility, existing.declaringClass, this, name)) { // It's effectively not there for us, so actualOverride remains false } else { actualOverride = true diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt index 3bbc5da..fe10726 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -68,7 +68,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { if (cls.className == "Obj") break val mangled = cls.mangledName(name) instanceScope.objects[mangled]?.let { rec -> - if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, cls, caller)) { + if (canAccessMember(rec.visibility, cls, caller, name)) { return resolveRecord(scope, rec, name, cls) } } @@ -77,8 +77,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { // 2. Unmangled storage instanceScope.objects[name]?.let { rec -> val decl = rec.declaringClass - // Unmangled access is only allowed if it's public OR we are inside the same instance's method - if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, decl, caller)) { + if (canAccessMember(rec.visibility, decl, caller, name)) { return resolveRecord(scope, rec, name, decl) } } @@ -174,7 +173,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { if (cls.className == "Obj") break val mangled = cls.mangledName(name) instanceScope.objects[mangled]?.let { rec -> - if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, cls, caller)) { + if (canAccessMember(rec.effectiveWriteVisibility, cls, caller, name)) { updateRecord(scope, rec, name, newValue, cls) return } @@ -184,7 +183,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { // 2. Unmangled storage instanceScope.objects[name]?.let { rec -> val decl = rec.declaringClass - if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, decl, caller)) { + if (canAccessMember(rec.effectiveWriteVisibility, decl, caller, name)) { updateRecord(scope, rec, name, newValue, decl) return } @@ -282,7 +281,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { } val decl = rec.declaringClass ?: cls val effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null - if (!canAccessMember(rec.visibility, decl, effectiveCaller)) + if (!canAccessMember(rec.visibility, decl, effectiveCaller, name)) scope.raiseError( ObjIllegalAccessException( scope, @@ -433,7 +432,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla // Visibility: declaring class is the qualified ancestor for mangled storage val decl = rec.declaringClass ?: startClass val caller = scope.currentClassCtx - if (!canAccessMember(rec.visibility, decl, caller)) + if (!canAccessMember(rec.visibility, decl, caller, name)) scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})")) return instance.resolveRecord(scope, rec, name, decl) } @@ -442,7 +441,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla instance.instanceScope[name]?.let { rec -> val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name) val caller = scope.currentClassCtx - if (!canAccessMember(rec.visibility, decl, caller)) + if (!canAccessMember(rec.visibility, decl, caller, name)) scope.raiseError( ObjIllegalAccessException( scope, @@ -456,7 +455,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name") val decl = r.declaringClass ?: startClass val caller = scope.currentClassCtx - if (!canAccessMember(r.visibility, decl, caller)) + if (!canAccessMember(r.visibility, decl, caller, name)) scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})")) return instance.resolveRecord(scope, r, name, decl) @@ -468,7 +467,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla instance.instanceScope.objects[mangled]?.let { f -> val decl = f.declaringClass ?: startClass val caller = scope.currentClassCtx - if (!canAccessMember(f.effectiveWriteVisibility, decl, caller)) + if (!canAccessMember(f.effectiveWriteVisibility, decl, caller, name)) ObjIllegalAccessException( scope, "can't assign to field $name (declared in ${decl.className})" @@ -482,7 +481,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla instance.instanceScope[name]?.let { f -> val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name) val caller = scope.currentClassCtx - if (!canAccessMember(f.effectiveWriteVisibility, decl, caller)) + if (!canAccessMember(f.effectiveWriteVisibility, decl, caller, name)) ObjIllegalAccessException( scope, "can't assign to field $name (declared in ${decl?.className ?: "?"})" @@ -495,7 +494,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name") val decl = r.declaringClass ?: startClass val caller = scope.currentClassCtx - if (!canAccessMember(r.effectiveWriteVisibility, decl, caller)) + if (!canAccessMember(r.effectiveWriteVisibility, decl, caller, name)) ObjIllegalAccessException(scope, "can't assign to field $name (declared in ${decl.className})").raise() if (!r.isMutable) scope.raiseError("can't assign to read-only field: $name") if (r.value.assign(scope, newValue) == null) r.value = newValue @@ -511,7 +510,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla memberFromAncestor(name)?.let { rec -> val decl = rec.declaringClass ?: startClass val caller = scope.currentClassCtx - if (!canAccessMember(rec.visibility, decl, caller)) + if (!canAccessMember(rec.visibility, decl, caller, name)) scope.raiseError(ObjIllegalAccessException(scope, "can't invoke method $name (declared in ${decl.className})")) val saved = instance.instanceScope.currentClassCtx instance.instanceScope.currentClassCtx = decl @@ -526,7 +525,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla instance.instanceScope[name]?.let { rec -> val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name) val caller = scope.currentClassCtx - if (!canAccessMember(rec.visibility, decl, caller)) + if (!canAccessMember(rec.visibility, decl, caller, name)) scope.raiseError( ObjIllegalAccessException( scope, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index 5f0fd64..52153ad 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -1363,7 +1363,7 @@ class MethodCallRef( val decl = hierarchyMember.declaringClass ?: base.objClass mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> val inst = obj as ObjInstance - if (!visibility.isPublic && !canAccessMember(visibility, decl, sc.currentClassCtx)) + if (!visibility.isPublic && !canAccessMember(visibility, decl, sc.currentClassCtx, name)) sc.raiseError(ObjIllegalAccessException(sc, "can't invoke non-public method $name")) callable.invoke(inst.instanceScope, inst, a) } @@ -1447,7 +1447,7 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef { val slot = if (hit) cachedSlot else resolveSlot(scope) if (slot >= 0) { val rec = scope.getSlotRecord(slot) - if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) { + if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) { // Not visible via slot, fallback to other lookups } else { if (PerfFlags.PIC_DEBUG_COUNTERS) { @@ -1499,7 +1499,7 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef { val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope) if (slot >= 0) { val rec = scope.getSlotRecord(slot) - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) { + if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) { scope.assign(rec, name, newValue) return } @@ -1608,7 +1608,7 @@ class FastLocalVarRef( val actualOwner = cachedOwnerScope if (slot >= 0 && actualOwner != null) { val rec = actualOwner.getSlotRecord(slot) - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) { + if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) { if (PerfFlags.PIC_DEBUG_COUNTERS) { if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++ } @@ -1652,7 +1652,7 @@ class FastLocalVarRef( val actualOwner = cachedOwnerScope if (slot >= 0 && actualOwner != null) { val rec = actualOwner.getSlotRecord(slot) - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) { + if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) { return scope.resolve(rec, name) } } @@ -1698,7 +1698,7 @@ class FastLocalVarRef( val actualOwner = cachedOwnerScope if (slot >= 0 && actualOwner != null) { val rec = actualOwner.getSlotRecord(slot) - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) { + if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) { scope.assign(rec, name, newValue) return } diff --git a/lynglib/src/commonTest/kotlin/OOTest.kt b/lynglib/src/commonTest/kotlin/OOTest.kt index 4014420..3d3c85b 100644 --- a/lynglib/src/commonTest/kotlin/OOTest.kt +++ b/lynglib/src/commonTest/kotlin/OOTest.kt @@ -776,4 +776,38 @@ class OOTest { assertEquals("success", result.toString(), "Parameter 'id' should shadow method 'id' in block") } + + @Test + fun testOverrideVisibilityRules1() = runTest { + eval(""" + interface Base { + abstract protected fun foo() + + fun bar() { + // it must see foo() as it is protected and + // is declared here (even as abstract): + foo() + } + } + class Derived : Base { + protected val suffix = "!" + + private fun fooPrivateImpl() = "bar" + + override protected fun foo() { + // it should access own private and all protected memberes here: + fooPrivateImpl() + suffix + } + } + class Derived2: Base { + private var value = 42 + + override protected fun foo() { + if( value < 10 ) 10 else value + } + } + assertEquals("bar!", Derived().bar()) + assertEquals(42, Derived2().bar()) + """.trimIndent()) + } } \ No newline at end of file