Compare commits

...

2 Commits

Author SHA1 Message Date
d4bb186f85 Obj.toKotlin tiny fix 2026-01-16 06:00:39 +03:00
7bc17037f9 1.2.1-SNAPSHOT: improved visibility check for inheritance 2026-01-15 18:18:35 +03:00
14 changed files with 116 additions and 55 deletions

View File

@ -2,6 +2,11 @@
### Unreleased ### 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 - Language: Added `return` statement
- `return [expression]` exits the innermost enclosing callable (function or lambda). - `return [expression]` exits the innermost enclosing callable (function or lambda).
- Supports non-local returns using `@label` syntax (e.g., `return@outer 42`). - 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. - Header-specified constructor arguments are passed to direct bases.
- Visibility enforcement under MI: - Visibility enforcement under MI:
- `private` visible only inside the declaring class body. - `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: - Diagnostics improvements:
- Missing member/field messages include receiver class and linearization order; hints for `this@Type` or casts when helpful. - 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. - Invalid `this@Type` reports that the qualifier is not an ancestor and shows the receiver lineage.

View File

@ -32,7 +32,7 @@ High-density specification for LLMs. Reference this for all Lyng code generation
val area get = π * r * r val area get = π * r * r
``` ```
- **Mandatory `override`**: Required for all members existing in the ancestor chain. - **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. - **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. - **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. - **Extensions**: `fun Class.ext()` or `val Class.ext get = ...`. Scope-isolated.

View File

@ -425,7 +425,7 @@ Key rules and features:
- Visibility - Visibility
- `private`: accessible only inside the declaring class body; not visible in subclasses and cannot be accessed via `this@Type` or casts. - `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 ## Abstract Classes and Members
@ -859,21 +859,29 @@ Private fields are visible only _inside the class instance_:
### Protected members ### 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).
``` Protected members are not available from unrelated contexts:
class A() {
protected fun ping() { "pong" } ```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()
} }
class B() : A() {
fun call() { this@A.ping() }
} }
val b = B() class Derived : Base {
assertEquals("pong", b.call()) override protected fun foo() { "ok" }
}
assertEquals("ok", Derived().bar())
// Unrelated access is forbidden, even via cast // 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 It is possible to provide private constructor parameters so they can be

View File

@ -1701,7 +1701,7 @@ assertEquals(null, (buzz as? Foo)?.runA())
Notes: 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. - 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. - Safe‑call `?.` works with `as?` for optional dispatch.
## Extension members ## Extension members

View File

@ -24,6 +24,9 @@ class Person(private var _age: Int) {
### Private and Protected Setters ### 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. 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 ```lyng
class Counter { class Counter {
var count = 0 var count = 0

View File

@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" 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 // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below

View File

@ -54,14 +54,14 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
for (cls in effectiveClass.mro) { for (cls in effectiveClass.mro) {
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null && !rec.isAbstract) { 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) return rec.copy(receiver = receiver)
} }
} }
} }
// Finally, root object fallback // Finally, root object fallback
Obj.rootObjectType.members[name]?.let { rec -> 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) return rec.copy(receiver = receiver)
} }
} }

View File

@ -112,7 +112,7 @@ open class Scope(
} }
} }
s.objects[name]?.let { rec -> 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 -> caller?.let { ctx ->
s.localBindings[ctx.mangledName(name)]?.let { rec -> s.localBindings[ctx.mangledName(name)]?.let { rec ->
@ -120,11 +120,11 @@ open class Scope(
} }
} }
s.localBindings[name]?.let { rec -> 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 -> s.getSlotIndexOf(name)?.let { idx ->
val rec = s.getSlotRecord(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 return null
} }
@ -163,7 +163,7 @@ open class Scope(
this.extensions[cls]?.get(name)?.let { return it } this.extensions[cls]?.get(name)?.let { return it }
} }
return thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec -> 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 if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null
else rec else rec
} else null } else null
@ -186,7 +186,7 @@ open class Scope(
s.extensions[cls]?.get(name)?.let { return it } s.extensions[cls]?.get(name)?.let { return it }
} }
s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec -> 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) { 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 // ignore fields, properties and abstracts here, they will be handled by the caller via readField
} else return rec } else return rec
@ -358,14 +358,14 @@ open class Scope(
for (cls in effectiveClass.mro) { for (cls in effectiveClass.mro) {
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null && !rec.isAbstract) { 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) return rec.copy(receiver = receiver)
} }
} }
} }
// Finally, root object fallback // Finally, root object fallback
Obj.rootObjectType.members[name]?.let { rec -> 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) return rec.copy(receiver = receiver)
} }
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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]. */ /** 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) { val res = when (visibility) {
Visibility.Public -> true Visibility.Public -> true
Visibility.Private -> (decl != null && caller === decl) Visibility.Private -> (decl != null && caller === decl)
@ -33,7 +38,14 @@ fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass
decl == null -> false decl == null -> false
caller == null -> false caller == null -> false
caller === decl -> true 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 return res

View File

@ -113,7 +113,7 @@ open class Obj {
if (rec != null && !rec.isAbstract) { if (rec != null && !rec.isAbstract) {
val decl = rec.declaringClass ?: cls val decl = rec.declaringClass ?: cls
val caller = scope.currentClassCtx 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 ?: "?"})")) scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
if (rec.type == ObjRecord.Type.Property) { if (rec.type == ObjRecord.Type.Property) {
@ -140,7 +140,7 @@ open class Obj {
cls.members[name]?.let { rec -> cls.members[name]?.let { rec ->
val decl = rec.declaringClass ?: cls val decl = rec.declaringClass ?: cls
val caller = scope.currentClassCtx 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 ?: "?"})")) scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
if (rec.type == ObjRecord.Type.Property) { if (rec.type == ObjRecord.Type.Property) {
@ -429,10 +429,10 @@ open class Obj {
} }
/** /**
* Convert Lyng object to its Kotlin counterpart * Convert a Lyng object to its Kotlin counterpart
*/ */
open suspend fun toKotlin(scope: Scope): Any? { open suspend fun toKotlin(scope: Scope): Any? {
return toString() return toString(scope).value
} }
fun willMutate(scope: Scope) { fun willMutate(scope: Scope) {
@ -482,7 +482,7 @@ open class Obj {
cls.members[name]?.let { rec -> cls.members[name]?.let { rec ->
val decl = rec.declaringClass ?: cls val decl = rec.declaringClass ?: cls
val caller = scope.currentClassCtx 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 ?: "?"})")) 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) val resolved = resolveRecord(scope, rec, name, decl)
if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement) if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement)
@ -526,7 +526,7 @@ open class Obj {
} }
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
// Check visibility for non-property members here if they weren't checked before // 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 ?: "?"})")) scope.raiseError(ObjIllegalAccessException(scope, "can't access field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
return obj return obj
} }
@ -574,7 +574,7 @@ open class Obj {
val decl = field.declaringClass val decl = field.declaringClass
val caller = scope.currentClassCtx 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 ?: "?"})")) scope.raiseError(ObjIllegalAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
if (field.type == ObjRecord.Type.Delegated) { if (field.type == ObjRecord.Type.Delegated) {
val del = field.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate") val del = field.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")

View File

@ -461,7 +461,7 @@ open class ObjClass(
if (existing != null && existing.declaringClass != this) { if (existing != null && existing.declaringClass != this) {
// If the existing member is private in the ancestor, it's not visible for overriding. // 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. // 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 // It's effectively not there for us, so actualOverride remains false
} else { } else {
actualOverride = true actualOverride = true

View File

@ -68,7 +68,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (cls.className == "Obj") break if (cls.className == "Obj") break
val mangled = cls.mangledName(name) val mangled = cls.mangledName(name)
instanceScope.objects[mangled]?.let { rec -> 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) return resolveRecord(scope, rec, name, cls)
} }
} }
@ -77,8 +77,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// 2. Unmangled storage // 2. Unmangled storage
instanceScope.objects[name]?.let { rec -> instanceScope.objects[name]?.let { rec ->
val decl = rec.declaringClass val decl = rec.declaringClass
// Unmangled access is only allowed if it's public OR we are inside the same instance's method if (canAccessMember(rec.visibility, decl, caller, name)) {
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, decl, caller)) {
return resolveRecord(scope, rec, name, decl) return resolveRecord(scope, rec, name, decl)
} }
} }
@ -174,7 +173,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (cls.className == "Obj") break if (cls.className == "Obj") break
val mangled = cls.mangledName(name) val mangled = cls.mangledName(name)
instanceScope.objects[mangled]?.let { rec -> 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) updateRecord(scope, rec, name, newValue, cls)
return return
} }
@ -184,7 +183,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// 2. Unmangled storage // 2. Unmangled storage
instanceScope.objects[name]?.let { rec -> instanceScope.objects[name]?.let { rec ->
val decl = rec.declaringClass 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) updateRecord(scope, rec, name, newValue, decl)
return return
} }
@ -282,7 +281,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} }
val decl = rec.declaringClass ?: cls val decl = rec.declaringClass ?: cls
val effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null 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( scope.raiseError(
ObjIllegalAccessException( ObjIllegalAccessException(
scope, scope,
@ -433,7 +432,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
// Visibility: declaring class is the qualified ancestor for mangled storage // Visibility: declaring class is the qualified ancestor for mangled storage
val decl = rec.declaringClass ?: startClass val decl = rec.declaringClass ?: startClass
val caller = scope.currentClassCtx 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})")) scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})"))
return instance.resolveRecord(scope, rec, name, decl) 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 -> instance.instanceScope[name]?.let { rec ->
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name) val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller)) if (!canAccessMember(rec.visibility, decl, caller, name))
scope.raiseError( scope.raiseError(
ObjIllegalAccessException( ObjIllegalAccessException(
scope, 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 r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
val decl = r.declaringClass ?: startClass val decl = r.declaringClass ?: startClass
val caller = scope.currentClassCtx 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})")) scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})"))
return instance.resolveRecord(scope, r, name, decl) 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 -> instance.instanceScope.objects[mangled]?.let { f ->
val decl = f.declaringClass ?: startClass val decl = f.declaringClass ?: startClass
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller)) if (!canAccessMember(f.effectiveWriteVisibility, decl, caller, name))
ObjIllegalAccessException( ObjIllegalAccessException(
scope, scope,
"can't assign to field $name (declared in ${decl.className})" "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 -> instance.instanceScope[name]?.let { f ->
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name) val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller)) if (!canAccessMember(f.effectiveWriteVisibility, decl, caller, name))
ObjIllegalAccessException( ObjIllegalAccessException(
scope, scope,
"can't assign to field $name (declared in ${decl?.className ?: "?"})" "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 r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
val decl = r.declaringClass ?: startClass val decl = r.declaringClass ?: startClass
val caller = scope.currentClassCtx 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() 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.isMutable) scope.raiseError("can't assign to read-only field: $name")
if (r.value.assign(scope, newValue) == null) r.value = newValue 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 -> memberFromAncestor(name)?.let { rec ->
val decl = rec.declaringClass ?: startClass val decl = rec.declaringClass ?: startClass
val caller = scope.currentClassCtx 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})")) scope.raiseError(ObjIllegalAccessException(scope, "can't invoke method $name (declared in ${decl.className})"))
val saved = instance.instanceScope.currentClassCtx val saved = instance.instanceScope.currentClassCtx
instance.instanceScope.currentClassCtx = decl instance.instanceScope.currentClassCtx = decl
@ -526,7 +525,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
instance.instanceScope[name]?.let { rec -> instance.instanceScope[name]?.let { rec ->
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name) val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller)) if (!canAccessMember(rec.visibility, decl, caller, name))
scope.raiseError( scope.raiseError(
ObjIllegalAccessException( ObjIllegalAccessException(
scope, scope,

View File

@ -1363,7 +1363,7 @@ class MethodCallRef(
val decl = hierarchyMember.declaringClass ?: base.objClass val decl = hierarchyMember.declaringClass ?: base.objClass
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
val inst = obj as ObjInstance 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")) sc.raiseError(ObjIllegalAccessException(sc, "can't invoke non-public method $name"))
callable.invoke(inst.instanceScope, inst, a) 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) val slot = if (hit) cachedSlot else resolveSlot(scope)
if (slot >= 0) { if (slot >= 0) {
val rec = scope.getSlotRecord(slot) 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 // Not visible via slot, fallback to other lookups
} else { } else {
if (PerfFlags.PIC_DEBUG_COUNTERS) { 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) val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
if (slot >= 0) { if (slot >= 0) {
val rec = scope.getSlotRecord(slot) 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) scope.assign(rec, name, newValue)
return return
} }
@ -1608,7 +1608,7 @@ class FastLocalVarRef(
val actualOwner = cachedOwnerScope val actualOwner = cachedOwnerScope
if (slot >= 0 && actualOwner != null) { if (slot >= 0 && actualOwner != null) {
val rec = actualOwner.getSlotRecord(slot) 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 (PerfFlags.PIC_DEBUG_COUNTERS) {
if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++ if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++
} }
@ -1652,7 +1652,7 @@ class FastLocalVarRef(
val actualOwner = cachedOwnerScope val actualOwner = cachedOwnerScope
if (slot >= 0 && actualOwner != null) { if (slot >= 0 && actualOwner != null) {
val rec = actualOwner.getSlotRecord(slot) 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) return scope.resolve(rec, name)
} }
} }
@ -1698,7 +1698,7 @@ class FastLocalVarRef(
val actualOwner = cachedOwnerScope val actualOwner = cachedOwnerScope
if (slot >= 0 && actualOwner != null) { if (slot >= 0 && actualOwner != null) {
val rec = actualOwner.getSlotRecord(slot) 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) scope.assign(rec, name, newValue)
return return
} }

View File

@ -776,4 +776,38 @@ class OOTest {
assertEquals("success", result.toString(), "Parameter 'id' should shadow method 'id' in block") 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())
}
} }