Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File accessibility modifier #5969

Closed
RikkiGibson opened this issue Mar 30, 2022 · 1 comment
Closed

File accessibility modifier #5969

RikkiGibson opened this issue Mar 30, 2022 · 1 comment
Labels
Proposal Question Question to be discussed in LDM related to a proposal

Comments

@RikkiGibson
Copy link
Contributor

Issue: #5529

LDM notes:

This proposal explores a more generalized file accessibility modifier as opposed to e.g. private class at the top level or only file private.

Detailed design

file accessibility domain

The modifier file is permitted on types and members within types. When determining the accessibility domain of a member M with the file modifier, we perform the following steps:

  1. Determine the accessibility domain D for member M using the definition given in the C# standard (linked above).
  2. Intersect D with the compilation unit in which M is declared.

Consequences of this include:

  1. file public and file internal declared accessibility both result in identical accessibility domains. Thus, it's disallowed to use file public as the declared accessibility of a member.
  2. file protected internal, file private protected and file protected declared accessibility all result in identical accessibility domains. Therefore, it's also disallowed to use file protected internal or file private protected as the declared accessibility of a member.

The allowed declared accessibility combinations include:

  • file internal, on top-level types and members within types
  • file protected on nested types and members within types
  • file private on nested types and members within types
  • open question: file, which uses the default accessibility depending on the context.

Members which don't allow any accessibility modifiers also cannot use the file modifier. For example, explicit interface implementations can't use the file modifier, and neither can interface members which lack a default implementation.

interface I
{
    void M1(); // ok
    file void M2(); // error
    file internal void M3() { } // ok
}

class C : I
{
    file I.M3() { } // error
}

Metadata type names

It's important that file types with the same name can be declared across multiple files:

// src/FileA.cs
namespace NS;

file internal class Program
{
}

// src/generated/FileB.g.cs
namespace NS;

file internal class Program // ok
{
}

The compiler needs to ensure that the runtime sees these as distinct types. We propose introducing a trailing unspeakable namespace a la the following:

// src/FileA.cs
namespace NS;

// Type name in metadata is: `NS.<>1_FileA.Program`
file internal class Program
{
}

// src/generated/FileB.g.cs
namespace NS;

// Type name in metadata is: `NS.<>2_FileB.g.Program`
file internal class Program
{
}

In the above, the ordinal of the file and the file's name minus its extension are used as a "trailing" namespace between the declared namespace and the type name in source. Usually we don't include an unspeakable naming scheme in the specification, but here it seems worth codifying since it will be fairly easy to observe the full name of a user-authored file accessible type with this feature. Note that the use of <> in the namespace also makes the type unspeakable when the assembly containing the type has IVT to another assembly.

This scheme also handles nested types:

// src/FileA.cs
namespace NS;

internal partial class C
{
    // Type name in metadata is: `NS.C+<>1_FileA.Program`
    file internal class Program
    {
    }
}

// src/generated/FileB.g.cs
namespace NS;

internal partial class C
{
    // Type name in metadata is: `NS.C+<>2_FileB.g.Program`
    file internal class Program
    {
    }
}

It might be possible to observe such file internal types through an IVT via reflection. For example, by enumerating the types in an assembly with a given accessibility. If this is something we need to defend against, then it's possible some revision of the metadata representation with input from the reflection libraries team would be needed.

Non-type member signatures

We propose introducing a synthesized modreq per-file which ensures the runtime can distinguish 'file' members across different files, without the compiler or program author needing to rename the members. This also prevents the members from being used across an IVT boundary.

// src/FileA.cs
internal partial class C
{
    file private void M() { }
}

// src/util/FileB.cs
internal partial class C
{
    file private void M() { } // ok
}

We emit IL like the following:

.class private auto ansi sealed beforefieldinit <>1_FileA
    extends [System.Private.CoreLib]System.Object
{
    // ...
}

.class private auto ansi sealed beforefieldinit <>2_FileB
    extends [System.Private.CoreLib]System.Object
{
    // ...
}

.class private auto ansi beforefieldinit C
    extends [System.Private.CoreLib]System.Object
{
    .method private hidebysig 
        instance void modreq(<>1_FileA) M () cil managed
    {
        // ...
    }

    .method private hidebysig 
        instance void modreq(<>2_FileB) M () cil managed
    {
        // ...
    }
}

It's worth noting here that modreqs can't be applied to types. Otherwise, we would probably be interested in a scheme which uses modreqs on both types and non-types for uniformity.

Alternative: Non-type member name collisions

If we don't want to synthesize modreqs, we could use an alternative design: the conventional rules around member name duplication apply, even when the members are declared across different files.

// File1.cs
partial class C
{
    file private void M() { }
}

// File2.cs
partial class C
{
    // error CS0111: Type 'C' already defines a member called 'M' with the same parameter types
    file private void M() { }
}

Non-type file internal members with InternalsVisibleTo

We would like to prevent file internal and file protected members from being used across assembly boundaries.

// Assembly1
[assembly: InternalsVisibleTo("Assembly2")]

public class C1
{
    file internal static void M() { }
}

// Assembly2
public class C2
{
    void M()
    {
        C1.M(); // error
    }
}
// Assembly1
public class C1
{
    file protected void M1() { }
}

// Assembly2
public class C2 : C1
{
    void M2()
    {
        M1(); // error
    }
}

To accomplish this, we propose introducing a modreq on members with the file modifier:

namespace System.Runtime.CompilerServices
{
    /// <summary>Indicates the member is only accessible from the file it was originally declared in.</summary>
    public sealed class FileAccessible { }
}

Interface implementation

When we considered allowing file public members, there was some concern about implicit interface implementation. For example:

interface I
{
    void M();
}

class Base : I
{
    public void M() { }
}

class Derived : Base, I
{
    file public void M() { }
}

By disallowing file public, we also disallow all file members from implementing interface members. Therefore, we don't expect to enter a situation where the runtime unexpectedly picks a file method for an interface implementation when some evil combination of interface lists, explicit implementations and file methods is used.

Default declared accessibility

Open question: should it be allowed to use file with the default accessibility?

// equivalent to 'file internal class C1'
file class C1
{
    // equivalent to 'file private void M()'
    file void M() { }
}

file partial members

Open question: should it be allowed to use partial with file?

// File1.cs
file partial class C
{
    void M1() { }
}

file partial class C
{
    void M2() { M1(); } // ok
}

The typical purpose of partial is to allow various members to be declared and implemented in different files. However, it doesn't seem to do harm to allow it here, and it doesn't seem like there's any design space which is protected by disallowing it. We would just want to ensure we give reasonable errors when file partial is used on the same method in different files.

namespace accessibility

#2497

Although it's not part of this proposal, there has been some interest in a separate proposal for an accessibility level which includes only program text within the same namespace and assembly as the declaration. We think that such a proposal wouldn't be able to work quite the same way as this one, since it seems important to make namespace internal types visible across IVTs, for example.

// Assembly1
[assembly: InternalsVisibleTo("Assembly2")]

namespace NS;

namespace internal class C
{
    public void M() { }
}

// Assembly2
namespace NS
{
    class Program
    {
        public static void Main()
        {
            var c = new C(); // ok
            c.M();
        }
    }
}

namespace NS2
{
    class Program
    {
        public static void Main()
        {
            var c = new C(); // error
            c.M();
        }
    }
}

In this case, most likely something like a [NamespaceAccessible] attribute with a [RequiredFeature] attribute on the namespace internal type, ensuring that any compilers that consume the type understand the associated access limitations.

@RikkiGibson RikkiGibson added the Proposal Question Question to be discussed in LDM related to a proposal label Mar 30, 2022
@333fred
Copy link
Member

333fred commented Mar 31, 2022

This version of the proposal was discussed in LDM on 03/30, and rejected: https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-30.md#file-private-accessibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Proposal Question Question to be discussed in LDM related to a proposal
Projects
None yet
Development

No branches or pull requests

2 participants