A reflective write to a private field is refused before it runs — which boundary refused, and what must its owner grant?
answer
- two layers, not one
- the compiler guards call sites only
- suppression is a request, not a rewrite
- coarse boundary is consulted first
- the declaring unit's owner grants the opening
basics
~20 sTwo layers guard a hidden member: the per-member access check, and, where a runtime groups types into packaged units, that unit's encapsulation boundary. A refusal of the suppression request itself points at the outer one, which only the unit's author or the deployer can open.
solid answer
~50 sVisibility is enforced twice. The compiler rejects a compiled call site that names a hidden member, and the reflective path — which has no compiled site, because the member is found by name at run time — repeats an equivalent check at the moment of access, against the identity of the code asking. Suppressing that check is a request to a run-time access policy, not a rewrite of the declaration: only the member handle you hold skips the check. Where a runtime also groups types into a packaged unit that declares what it exposes, that coarser boundary is evaluated first, and a unit whose package was never opened for deep reflective access refuses the request outright. Lifting it is the unit author's call, in the unit's own descriptor, or the deployer's, through an explicit launch-time opening.
code
pseudocode · 12 lineshandle = memberHandle(HiddenType, "secret")
// asks the run-time access policy; the packaging
// boundary around HiddenType is consulted first
granted = handle.requestSuppressCheck()
if not granted:
// the declaring unit never opened that package;
// no per-member request can override that answer
report("boundary refused before any access")
else:
handle.write(target, value) // only THIS handle is uncheckedgo deeper
Remember that a private member is not physically hidden — it is guarded by a check, and the guard is asked, not defeated. Reaching one at run time is something a tool does deliberately, not something ordinary code can stumble into.
Be able to separate the compile-time rejection of a call site from the run-time access check that the reflective path performs, and to say that suppression rides on a single handle rather than on the declaration.
Diagnose the refusal in a real deployment: name whether the request or the access failed, say which layer owns the answer, and choose the narrowest opening that unblocks the tool rather than a blanket one.
Own the policy. Decide which packages your units open and to whom, knowing that an opening is granted to the package for the whole process, and that a fixture requiring a closed boundary to open is usually reporting a missing construction seam.
Reaching past a visibility rule at run time is not a single act of force. It is a request, and where the runtime has more than one layer of encapsulation, each layer gets to refuse it independently. Diagnosing a refusal starts with working out which layer spoke. ## The per-member access check A member declared hidden is enforced first at compile time: a call site in other code that names it does not compile. That enforcement is **static** — it belongs to the compiler, and it is gone by the time the program runs. A reflective access has no compiled call site. The member is located by name while the program is running, so the runtime performs an equivalent check **at the moment of access**, comparing the declared accessibility of the member against the identity of the code performing the access. This is the check a fixture asks to suppress. Two properties of that suppression matter and are routinely misremembered: - It is a **request**, evaluated by a run-time access policy, not a switch that always succeeds. - It changes **only the handle** on which it was granted. The declaration is untouched, other compiled call sites still fail to compile, and a freshly obtained handle for the same member starts out checked again. ## The packaging boundary above the type Many runtimes add a coarser unit above the type: a deployable unit that names which of its packages are visible to other units and — separately — which are open to deep reflective access. Others have no such layer at all, and there the member check is the only gate. Where the layer exists, the ordering is what surprises people. The coarse boundary is consulted **before** the member's own modifiers, so: 1. a unit that never opened the package refuses the suppression request even for a member the requester could otherwise reach; 2. the refusal lands on the **request**, before any read or write is attempted, which is why the failure looks like a permission error rather than a bad value; 3. relaxing the member's own visibility does not help, because that decision was already overruled one level up. ## What "granting an opening" actually means The opening is a property of the **declaring** unit at run time, not of the caller. It comes from one of: - a declaration in the unit's own descriptor opening a package, either to every consumer or to one named consumer; - a blanket opening of the whole unit by its author; - a deployment-time instruction supplied by whoever launches the process, opening a package the author chose not to open. The third form is why the same fixture code passes in one packaging and fails in another: nothing about the code changed, only the environment that grants the opening. ## Reading the refusal | What refused | How it shows | Who can lift it | |---|---|---| | the per-member access check | the read or write fails for this caller | the caller, by requesting suppression, if the policy permits | | the packaging boundary | the request to suppress fails, before any access | the unit's author in its descriptor, or the deployer at launch | | a hardened run-time policy | the request fails for every caller alike | the process owner, by changing the policy | | a write-once declaration | the request succeeds but the write is rejected or unobservable | nobody — this one is the language's promise, not a permission | ## Why this shapes how a fixture is written A fixture that forces hidden state has taken on an **environmental** dependency, not just a code one. Practical consequences: - The same fixture can pass locally and fail once the code ships inside a stricter packaging. - An opening is granted to the package, not to the tool that asked for it, so widening it for one fixture widens it for everything running in the process. - The narrow opening — one package, one named consumer — is almost always available and almost always preferable to a blanket one. - If a fixture can only be made to work by opening a boundary the owner deliberately closed, that is a design signal: the type has no published way to reach a state the tests need. The short version to carry into an interview: the compiler guards call sites, the runtime guards accesses, and a packaging unit — where one exists — guards whole packages. Suppression answers only the middle one.
- If a runtime has no packaging layer above the type, what still limits a forced write?Three things remain. The per-member access check still runs, and the run-time access policy may refuse to suppress it. A field declared assignable only during construction may reject the write or make it unobservable. And only code inside the same process can ask at all — nothing about suppression reaches across a process boundary.
- Why does opening a package for one tool widen the blast radius beyond that tool?Because the opening is a property of the declaring unit while the process runs, not a grant to the requester. Once a package is open for deep reflective access, any code loaded in that process can obtain handles to its hidden members. Opening to one named consumer, where the runtime supports it, keeps the grant narrow.
- Does suppressing the check on one member handle affect the next handle for the same member?No. The suppression rides on the handle, not on the declaration or the member itself. Code that looks the member up again gets a checked handle and must ask again. This is also why caching a suppressed handle is common — and why an audit that greps for the suppression call still sees every site.
saying these in an interview costs you the question
- Thinks suppressing the check rewrites the member's declared visibility
- Believes a granted suppression applies process-wide to every handle
- Assumes a member's own modifiers decide a refusal that came from the packaging unit
- Expects a fixture that passes locally to pass under any packaging
- Thinks relaxing the field's modifier fixes a refused suppression request
- Believes only the compiler ever enforces visibility, so run time is unguarded