## Name Sub::Private - Private subroutines and methods ## Version Version 0.06 ## Synopsis ```perl package Foo; use Sub::Private; sub foo { return 42 } sub bar :Private { return foo() + 1; } sub baz { return bar() + 1; } ``` ## Description Enforces strictly private access on subroutines. A subroutine decorated with `:Private` (or named in `use Sub::Private qw(...)` when in enforce mode) may only be called from within its defining package. Subclasses do not inherit access: private means _this package only_. ### Two Enforcement Modes - `namespace` mode (default, backward-compatible) Removes the subroutine from the package symbol table using [namespace::clean](https://metacpan.org/pod/namespace%3A%3Aclean). Direct (non-method) function calls compiled before cleanup still work because Perl optimises them to direct opcode references. OO method dispatch (`$self-`name>) does not work for private subs in this mode because method lookup uses the symbol table at runtime. This is the default mode and is backward-compatible with all existing code. - `enforce` mode (OO-safe, opt-in) Replaces the subroutine with a wrapper closure that checks `caller` at call time and either delegates (owner package) or croaks (anyone else). Works correctly with OO dispatch (`$self-`\_helper>). Enable before declaring your first private sub: ```perl BEGIN { $Sub::Private::config{mode} = 'enforce' } package MyClass; use Sub::Private; sub _helper :Private { ... } ``` ### Behaviour of Wrapped Subs (Enforce Mode) - The wrapper hands over with `goto &sub`, so it does not appear on the call stack: `caller` inside a private sub sees its real caller. - Arguments, `@_` aliasing and calling context (list, scalar or void) are passed through unchanged. - `$_` is left untouched, whether the call is allowed or blocked. - A blocked call croaks; it does not fall through to `AUTOLOAD`. - Works with [Moo](https://metacpan.org/pod/Moo) and [Moose](https://metacpan.org/pod/Moose) classes; use the declarative form after `use Moo` or `use Moose`. ### Loading at Run Time Packages that use `Sub::Private` may be loaded at run time (`require`, a plugin loader, a string `eval`), and `Sub::Private` itself may be loaded for the first time that way. The private subs are then wrapped (or removed, in `namespace` mode) when the enclosing scope, normally the rest of the file, has been compiled. ### Bypass for Testing Either condition alone (OR logic) disables all access checks in enforce mode: - `$Sub::Private::BYPASS` set to a true value. Use `local` in tests. - `$ENV{HARNESS_ACTIVE}` set (the convention used by [Test::Harness](https://metacpan.org/pod/Test%3A%3AHarness)/prove). `$Sub::Private::BYPASS` is the recommended form for new test code. The `HARNESS_ACTIVE` bypass can be disabled: ``` $Sub::Private::config{harness_bypass} = 0; ``` ### Configuration ``` $Sub::Private::config{mode} -- 'namespace' (default) or 'enforce' $Sub::Private::config{harness_bypass} -- 1 (default); set to 0 to test enforcement ``` ### Error Message Format (Enforce Mode) ``` bar() is a private subroutine of Foo and cannot be called from Bar ``` ## Public Interface ### Import ```perl use Sub::Private; # attribute form -- no arguments use Sub::Private qw(_a _b _c); # declarative form (enforce mode only) ``` #### Purpose Called automatically by `use Sub::Private`. With **no arguments**: makes the `:Private` attribute globally available via `UNIVERSAL`. No other action is taken. With **one or more sub names**: registers those named subs in the calling package for access-enforcement wrapping at `CHECK` time. If `CHECK` has already fired (for example the package is loaded with `require` at run time), the subs are wrapped as soon as the enclosing scope (normally the rest of the file) has been compiled, so the `use` line can still come before the subs it names. A direct `Sub::Private->import(...)` call at run time wraps immediately. Requires `$Sub::Private::config{mode}` to equal `'enforce'`; croaks otherwise. #### Arguments - `@subs` (optional) Zero or more Perl sub names. Each must be a defined, non-reference scalar matching `/\A[_a-zA-Z]\w*\z/`. `undef`, references, empty strings, and names starting with a digit or containing hyphens are all rejected. #### Returns The class name (`'Sub::Private'`) as a plain string in all cases. #### Side Effects - Pre-CHECK: appends `[$owner_pkg, $sub_name]` pairs to the internal `@_pending` list. - Post-CHECK, during compilation: installs wrapper closures in the calling package's stash when the enclosing scope has been compiled. - Post-CHECK, at run time: installs wrapper closures directly in the calling package's stash. #### Example ```perl BEGIN { $Sub::Private::config{mode} = 'enforce' } package MyClass; use Sub::Private qw(_helper _init); sub new { bless {}, shift } sub _helper { ... } # wrapped at CHECK time sub _init { ... } # wrapped at CHECK time sub run { my $s = shift; $s->_helper; $s->_init } ``` #### Api Specification ##### Input ```perl # No-argument form: always valid. Sub::Private->import(); # Declarative form (enforce mode only): { subs => { type => 'array', optional => 1, element => { type => 'string', regex => qr/\A[_a-zA-Z]\w*\z/, }, } } ``` ##### Output ```perl { type => 'string' } # returns the class name 'Sub::Private' ``` #### Messages ```perl Message Meaning / Action --------------------------------------------------- ----------------------------------------------- "Sub::Private->import: declarative form requires use Sub::Private qw(...) was called while mode => 'enforce'" $config{mode} is not 'enforce'. Set $config{mode} = 'enforce' in a BEGIN block before "use Sub::Private". "Sub::Private->import: 'NAME' is not a valid The sub name failed the identifier regex. Perl identifier" Check for typos, hyphens, leading digits, undef, or reference values in the import list. "Sub::Private: PKG::NAME is not defined" The named sub was not found in the stash at wrap time. Define the sub before import() runs, before CHECK fires, or (after CHECK) before the end of the enclosing scope. After CHECK, the location reported is the "use Sub::Private" line. ``` ## Public Variables ### `$Bypass` Set to a true value to disable all access checks (enforce mode only). Use `local` in tests; see ["Bypass for testing"](#bypass-for-testing). ### `%Config` Module-level configuration hash. Supported keys: - `mode` `'namespace'` (default) or `'enforce'`. Must be set in a `BEGIN` block before `use Sub::Private` to take effect at `CHECK` time. - `harness_bypass` When true (default), access checks are skipped whenever `$ENV{HARNESS_ACTIVE}` is set. Set to 0 to test enforcement under `prove`. ## Known Limitations - `namespace` mode: OO dispatch fails for private subs `$self-`\_helper> from within the owner package fails because method dispatch uses the symbol table at runtime, which no longer contains the entry. Use `enforce` mode for OO classes. - `enforce` mode: runtime-only Checks are runtime only; there is no compile-time enforcement. - `enforce` mode: raw coderef bypass A raw code reference obtained **before** wrapping (via `can()` or `\&Foo::_helper`) bypasses the check. The attribute form makes this hard because wrapping happens at CHECK time. When the package is loaded after CHECK, wrapping happens at the end of the enclosing scope, so a `BEGIN` block earlier in the same file could still take a reference to the unwrapped sub. - `enforce` mode: `can()` leaks private method existence In `enforce` mode the original sub is replaced by a wrapper closure, so `->can('_helper')` returns the wrapper (a true value) even to callers outside the owner package. In `namespace` mode the stash entry is deleted entirely, so `->can` correctly returns `undef`. A future release may inject a caller-aware `can()` override into each class that uses `enforce` mode, returning the coderef only when the caller is the owner package and `undef` for everyone else. - UNIVERSAL namespace pollution The `:Private` attribute is installed in `UNIVERSAL`, which is intentional (any package can use it after a single `use`), but it does introduce `UNIVERSAL::Private` into the global namespace. ## Dependencies Perl 5.14 or later, [Carp](https://metacpan.org/pod/Carp) (core), [Attribute::Handlers](https://metacpan.org/pod/Attribute%3A%3AHandlers) (core), [B::Hooks::EndOfScope](https://metacpan.org/pod/B%3A%3AHooks%3A%3AEndOfScope), [Readonly](https://metacpan.org/pod/Readonly), [Params::Validate::Strict](https://metacpan.org/pod/Params%3A%3AValidate%3A%3AStrict), [Return::Set](https://metacpan.org/pod/Return%3A%3ASet), [namespace::clean](https://metacpan.org/pod/namespace%3A%3Aclean), [Sub::Identify](https://metacpan.org/pod/Sub%3A%3AIdentify). ## See Also - [Test Dashboard](https://nigelhorne.github.io/Sub-Private/coverage/) - [Sub::Protected](https://metacpan.org/pod/Sub%3A%3AProtected) Sister module enforcing protected (owner + subclass) rather than strictly private access - [Sub::Abstract](https://metacpan.org/pod/Sub%3A%3AAbstract) Sister module enforcing abstract (virtual) methods - [namespace::clean](https://metacpan.org/pod/namespace%3A%3Aclean) ## Formal Specification The following Z-notation schemas formally specify the `CheckAccess` operation and `import`. ### `CheckAccess` ```perl -- Type abbreviations Package == seq CHAR -- a non-empty Perl package name string SubName == seq CHAR -- a Perl identifier string -- Private-access predicate (strictly owner only -- no isa expansion) permitted : Package x Package -> BOOL forall caller, owner : Package . permitted(caller, owner) <=> caller = owner -- System state +-Registry-------------------------------------------+ | private : P (Package x SubName) | | bypass : BOOL | | config : { mode : seq CHAR, | | harness_bypass : BOOL } | +----------------------------------------------------+ -- Initial state +-InitRegistry---------------------------------------+ | Registry | |----------------------------------------------------| | private = {} | | bypass = false | | config = { mode |-> 'namespace', | | harness_bypass |-> true } | +----------------------------------------------------+ -- Bypass predicate bypass_active(R) <=> R.bypass or (R.config.harness_bypass and HARNESS_ACTIVE) -- Access check: no state change +-CheckAccess----------------------------------------+ | Xi-Registry | | caller? : Package | | owner? : Package | | name? : SubName | | ok! : BOOL | |----------------------------------------------------| | (owner?, name?) in private | | ok! <=> bypass_active or permitted(caller?, owner?)| +----------------------------------------------------+ -- Violation (croak case): -- not ok! => -- croak("name?()" ++ " is a private subroutine of " ++ owner? -- ++ " and cannot be called from " ++ caller?) -- Key difference from Sub::Protected: -- permitted(caller, owner) <=> caller = owner (identity only) -- vs Sub::Protected: -- permitted(caller, owner) <=> owner in anc(caller) (ISA chain) ``` ### Import ```perl -- Valid identifier predicate valid_id : SubName -> BOOL valid_id(n) <=> n =~ /\A[_a-zA-Z]\w*\z/ -- Compile phase: CHECK has run, and code is still being compiled post_check : BOOL compiling : BOOL -- true while $^S is undefined -- Pre-condition (declarative form) +-ImportPre-----------------------------------------+ | config.mode = 'enforce' | | forall n in subs . valid_id(n) | +---------------------------------------------------+ -- Post-condition (before CHECK): queue for the CHECK block +-ImportPost_PreCheck-------------------------------+ | not post_check | |---------------------------------------------------| | pending' = pending ++ < (caller, n) | n in subs > | +---------------------------------------------------+ -- Post-condition (after CHECK, at run time): wrap now +-ImportPost_RunTime--------------------------------+ | post_check and not compiling | | forall n in subs . defined(caller, n) | |---------------------------------------------------| | forall n in subs . | | stash'(caller, n) = wrapper(caller, n) | +---------------------------------------------------+ -- Post-condition (after CHECK, while compiling): wrap at the -- end of the enclosing scope S +-ImportPost_Deferred-------------------------------+ | post_check and compiling | |---------------------------------------------------| | at end_of_scope(S) : | | (forall n in subs . defined(caller, n)) => | | forall n in subs . | | stash'(caller, n) = wrapper(caller, n) | | (exists n in subs . not defined(caller, n)) => | | croak at the "use" line | +---------------------------------------------------+ -- In every case, a sub that is not defined when wrapping is -- attempted croaks: -- "Sub::Private: " ++ caller ++ "::" ++ n ++ " is not defined" ``` ## Author Original Author: Peter Makholm, `` Current maintainer: Nigel Horne, `` ## Bugs Please report any bugs or feature requests to `bug-sub-private at rt.cpan.org`, or through the web interface at [https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Sub-Private](https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Sub-Private). ## Support ``` perldoc Sub::Private ``` - RT: CPAN's request tracker [https://rt.cpan.org/NoAuth/Bugs.html?Dist=Sub-Private](https://rt.cpan.org/NoAuth/Bugs.html?Dist=Sub-Private) - MetaCPAN [https://metacpan.org/dist/Sub-Private](https://metacpan.org/dist/Sub-Private) ## Copyright & License Copyright 2009 Peter Makholm, all rights reserved. Portions copyright 2024-2026 Nigel Horne. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.