Skip to content

Add structured Publicize item metadata - #234

Merged
krafs merged 16 commits into
mainfrom
structured-scope-syntax
Aug 4, 2026
Merged

krafs merged 16 commits into
mainfrom
structured-scope-syntax

Conversation

@krafs

@krafs krafs commented Jul 26, 2026 •

Copy link
Copy Markdown
Owner

Adds a structured item form where Namespace and Type are their own metadata instead of being packed into the Include string. Additive — the colon-string form is unchanged.

<Publicize Include="MyAssembly" Namespace="MyNamespace" />
<Publicize Include="MyAssembly" Namespace="MyNamespace" Type="MyType.MyNestedType" />
<Publicize Include="MyAssembly" Namespace="MyNamespace" Type="MyType{TKey,TValue}" />

Splitting Namespace out is what removes the namespace-vs-nested-type ambiguity, so dots inside Type always mean nesting. docs/publicization-semantics.md has the full behavior, including why the reserved qualifiers are rejected rather than ignored, and why several things that would otherwise be accepted are refused instead.

  • Naming a type structurally sweeps its members; Include="Asm:N.T" still publicizes only the type. Deliberate divergence, and what retires the wart that "all members of one type" was regex-only.
  • A colon-form DoNotPublicize on a type outranks every scope, because that form's behavior is frozen. An assembly-wide DoNotPublicize does not: it is the loosest scope there is, so a scope naming part of the assembly is more specific and carves an exception out of it. That matches the carve-out a colon-form member target has always had over an assembly deny, so the answer no longer depends on which syntax spelled an equally specific target, and "deny the assembly except namespace N" stays expressible. The composition hazard that argues for a veto — a shared .props denies, a consumer reopens — is not specific to scopes and is not fixable by one, since the colon form leaks the same way; it is reported as a warning instead.
  • The characterization and signature-closure suites are unmodified, which is the evidence that moving sweep settings from assembly-global to per-scope preserved behavior.
  • Fixes a latent bug: Hasher did not cover the new scopes, so two different Type targets would have shared a cache entry. The colon-form sets have a second collision of the same shape, tracked separately — fixing that one invalidates every existing cache entry, so it does not belong here.
  • SweepSettings and PublicizeScope are init-only, since one SweepSettings is resolved per scope and then shared by every type that scope covers. That needs an IsExternalInit polyfill on netstandard2.0; the net10.0 test projects bind their own through InternalsVisibleTo without ambiguity.

Three things are refused rather than resolved, on the same principle: anything this form accepts now, it accepts forever, and any semantics it leaves observable-but-undecided gets depended on.

  • MemberPattern on a scope. The regex matches dnlib's reflection name — + for nesting, `2 for arity — which is the exact spelling Type refuses in the same item, so honoring it on a scope would freeze that spelling in a second place and recreate the two-spellings fork one level down. The assembly-level pattern is unchanged and still applies inside every scope, and it is already slated for re-anchoring or removal; extending it here would only widen what that has to migrate.
  • A scope nested in another scope that leaves one of its filters unset. Whether it should inherit from the enclosing scope or from the assembly is genuinely open, and the two readings differ only in which members end up public — so choosing now and changing later would silently rewrite working builds, with no error and no warning. Setting the filter explicitly on the inner scope costs one attribute and keeps both answers available. Assembly-to-scope inheritance is unaffected; that one is decided and pinned.
  • A nameless type argument, as in Pair{,}. Only the count is read today, so it would work, but the names are reserved for Parameters and tightening this once they mean something would break items that build now.

No README yet. The syntax is only half-landed until member-level metadata arrives, and documenting it now would mean shipping a contract that has to explain what it cannot do. Note this makes the release, not the README, the point where these semantics freeze — worth deciding deliberately before this ships in one.

krafs added 11 commits August 3, 2026 20:45
Additive: the colon-string Include form is unchanged and still the
public contract. Both forms parse into the same AssemblyPlan.

Naming a type structurally sweeps its members, while the colon form
publicizes only the type itself. The divergence is deliberate — it is
what makes "all members of this type" expressible without a regex —
and both readings are supported permanently.

Sweep settings move from assembly-global to per-scope so a namespace
or type can carry its own filters. With no structured items present
this resolves to the old two rungs, which is why the characterization
suite is untouched.

Generic arity is spelled with braces only; a backtick is rejected so
that Parameters never has to reconcile two spellings later.
Commas inside a nested argument list were counted as arity, so
Holder{Dictionary{K,V}} lowered to Holder`2 and matched nothing.
IncludeSubNamespaces and IncludeTypeContents are settled in the design
but not built. Ignoring them would publicize more than the author asked
for, which is the one failure mode this parser is strict about.
Readability pass over the review comments: the type-name lowering loop
split into a segment scan plus a per-segment rewrite, error accumulation
made explicit instead of leaning on Fail's return value, and the scope
tie-break and prefix-coverage checks spelled out rather than packed into
compound conditions.

The design rationale that was duplicated across doc comments now lives
only in docs/publicization-semantics.md.
Resolution ranked a scope above the assembly, so adding a structured
Publicize item could publicize a namespace inside an assembly that a
DoNotPublicize item had denied. The frozen forms exist to prevent that
widening, so the assembly deny keeps the precedence its type-level
counterpart already has.

Also drops the namespace copy a type scope carried: the reflection name
already holds it, and nothing read the second copy.
Reverts the veto from eb90abe. Its rationale was that a scope overriding
an assembly deny would let the structured form widen what gets
publicized -- but rung 3 has always allowed exactly that for colon-form
targets, so the veto did not uphold the principle, it applied it to one
of the two forms. Two targets of identical specificity resolved
differently based only on which syntax spelled them.

The assembly rule is now just the loosest scope, which is where Resolve
already fell through to, so the drop was all that had to go. That leaves
one precedence mechanism instead of a lattice plus an out-of-band drop
-- the second mechanism being the kind of thing the rewrite exists to
retire.

The real hazard was always composition (a shared .props denies, a
consumer reopens), which a veto on scopes could not have addressed
anyway since the colon form leaks the same way. Reported as a warning
instead.
@krafs
krafs force-pushed the structured-scope-syntax branch from e863f7b to 2f262c1 Compare August 3, 2026 18:47
A deny scope naming a type is the same statement as the colon form, so it
gets the same absolute treatment. Namespace scopes deliberately do not
stop the walk: they name no type, and stopping would leave an explicitly
publicized nested type public but unreachable.
@krafs
krafs force-pushed the structured-scope-syntax branch from 58d71b8 to 0ca8465 Compare August 3, 2026 19:03
krafs added 4 commits August 3, 2026 21:06
The doc was chartered as a characterization baseline for a future
rewrite. The structured form is that rewrite, and it is being specified
in the same file, so the header now describes both jobs and where design
rationale does and does not belong.
Only the count is read today, so 'Pair{,}' worked. The names are
reserved for 'Parameters', and tightening this once they mean
something would break items that build now.
Whether a scope nested in another inherits that scope's filters or the
assembly's is undecided. The two readings differ only in which members
end up public, so picking one now and changing it later would silently
rewrite working builds. Rejecting the ambiguous item defers the choice
for the price of one attribute.
The regex matches dnlib's reflection name, the '+' and backtick spelling
'Type' refuses in the same item, so honoring it on a scope would freeze
that spelling in a second place. The assembly-level pattern is already
slated to be re-anchored or dropped; extending it to scopes only widens
what that has to migrate. Assembly-level behavior is unchanged and still
applies inside scopes.
@krafs
krafs marked this pull request as ready for review August 4, 2026 16:37
@krafs
krafs merged commit 21d74c7 into main Aug 4, 2026
10 checks passed
@krafs
krafs deleted the structured-scope-syntax branch August 4, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant