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

Syntax API for FileScopedNamespaces #54674

Closed
RikkiGibson opened this issue Jul 8, 2021 · 13 comments
Closed

Syntax API for FileScopedNamespaces #54674

RikkiGibson opened this issue Jul 8, 2021 · 13 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented Area-Compilers blocking API needs to reviewed with priority to unblock work Concept-API This issue involves adding, removing, clarification, or modification of an API. Feature Request New Language Feature - File Scoped Namespaces
Milestone

Comments

@RikkiGibson
Copy link
Contributor

RikkiGibson commented Jul 8, 2021

Background and Motivation

The file scoped namespaces feature #49000 introduces new syntax nodes. Those syntax nodes are naturally part of the public API.

Proposed API

namespace Microsoft.CodeAnalysis.CSharp.Syntax
{
+    public abstract partial class BaseNamespaceDeclarationSyntax : MemberDeclarationSyntax
+    {
+        public abstract SyntaxToken NamespaceKeyword { get; }
+        public abstract NameSyntax Name { get; }
+        public abstract SyntaxList<ExternAliasDirectiveSyntax> Externs { get; }
+        public abstract SyntaxList<UsingDirectiveSyntax> Usings { get; }
+        public abstract SyntaxList<MemberDeclarationSyntax> Members { get; }
+    }

-    public sealed partial class NamespaceDeclarationSyntax : MemberDeclarationSyntax { }
+    public sealed partial class NamespaceDeclarationSyntax : BaseNamespaceDeclarationSyntax
    {
-        public SyntaxToken NamespaceKeyword { get; }
+        public override SyntaxToken NamespaceKeyword { get; }
        // same change for the rest of the abstract members of `BaseNamespaceDeclarationSyntax`...
    }

+    public sealed partial class FileScopedNamespaceDeclarationSyntax : BaseNamespaceDeclarationSyntax
+    {
+        public override SyntaxToken NamespaceKeyword { get; }
        // same change for the rest of the abstract members of `BaseNamespaceDeclarationSyntax`...

+        public SyntaxToken SemicolonToken { get; }
+    }
}

namespace Microsoft.CodeAnalysis.CSharp
{
    public enum SyntaxKind
    {
        ...,
        NamespaceDeclaration,
+        FileScopedNamespaceDeclaration
    }
}

Usage Examples

public class MyVisitor : CSharpSyntaxVisitor
{
    public override void VisitNamespaceDeclaration(NamespaceDeclarationSyntax @namespace)
    {
        VisitBaseNamespaceDeclaration(@namespace);
    }

    public override void VisitFileScopedNamespaceDeclaration(FileScopedNamespaceDeclarationSyntax fileScopedNamespace)
    {
        VisitBaseNamespaceDeclaration(fileScopedNamespace);
    }

    private void VisitBaseNamespaceDeclaration(BaseNamespaceDeclarationSyntax baseNamespace)
    {
        foreach (var member in baseNamespace.Members)
        {
            DoSomethingWith(member);
        }
    }
}
static class SyntaxNodeExtensions
{
    public static IEnumerable<MemberDeclarationSyntax> GetMembers(this SyntaxNode? node)
    {
        switch (node)
        {
            case CompilationUnitSyntax compilation:
                return compilation.Members;
            case BaseNamespaceDeclarationSyntax @namespace:
                return @namespace.Members;
            case TypeDeclarationSyntax type:
                return type.Members;
            case EnumDeclarationSyntax @enum:
                return @enum.Members;
        }

        return SpecializedCollections.EmptyEnumerable<MemberDeclarationSyntax>();
    }
}

Alternative Designs

We might be able to pack the new syntax into the existing NamespaceDeclarationSyntax. However, to do this while maintaining invariants is a bit clumsy, and it more or less violates our guidelines around using the same node to represent two different things.

namespace Microsoft.CodeAnalysis.CSharp.Syntax
{
    public sealed partial class NamespaceDeclarationSyntax : MemberDeclarationSyntax
    {
        public SyntaxToken NamespaceKeyword { get; }
        public NameSyntax Name { get; }
+        public SyntaxToken LeadingSemicolonToken { get; }
        public SyntaxToken OpenBraceToken { get; }
        public SyntaxList<ExternAliasDirectiveSyntax> Externs { get; }
        public SyntaxList<UsingDirectiveSyntax> Usings { get; }
        public SyntaxList<MemberDeclarationSyntax> Members { get; }
        public SyntaxToken CloseBraceToken { get; }
        public SyntaxToken SemicolonToken { get; }
    }
}

Note particularly that this alternative design would require keeping the existing, optional SemicolonToken which trails the closing brace of the namespace, as well as adding a new LeadingSemicolonToken, which precedes the list of externs+usings+members in the namespace.

Risks

If we ship the suggested design, users will have to react by changing their code:

  • If they were looking for SyntaxKind.NamespaceDeclaration, they will probably want to look for both SyntaxKind.NamespaceDeclaration and SyntaxKind.FileScopedNamespaceDeclaration
  • If they were looking for NamespaceDeclarationSyntax, they will probably want to switch to BaseNamespaceDeclarationSyntax
  • If they were inheriting from the CSharpSyntaxVisitor, they will probably want to override both Visit(NamespaceDeclarationSyntax) and Visit(FileScopedNamespaceDeclarationSyntax), likely delegating to the same method for both.

However, there are some cases we've spotted internally, like the normalizer and formatter, where we definitely wouldn't want to treat these nodes the same for determining indent depth, for example.

This design also matches some past changes to the syntax design, such as for RecordDeclaration or ForEachStatement.

@RikkiGibson RikkiGibson added Concept-API This issue involves adding, removing, clarification, or modification of an API. Feature Request labels Jul 8, 2021
@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Jul 8, 2021
@jaredpar jaredpar added New Language Feature - File Scoped Namespaces and removed untriaged Issues and PRs which have not yet been triaged by a lead labels Jul 8, 2021
@jaredpar jaredpar added this to the 17.0 milestone Jul 8, 2021
@333fred
Copy link
Member

333fred commented Jul 8, 2021

@CyrusNajmabadi I assume you're good with this api proposal as well?

@333fred 333fred added api-ready-for-review API is ready for review, it is NOT ready for implementation blocking API needs to reviewed with priority to unblock work labels Jul 8, 2021
@CyrusNajmabadi
Copy link
Member

Yes i am.

@tmat
Copy link
Member

tmat commented Jul 9, 2021

Why are the common members abstract?

@RikkiGibson
Copy link
Contributor Author

Why are the common members abstract?

I think this is a convention of syntax generation--for <Field>s underneath <AbstractNode>s in Syntax.xml, we produce only abstract properties. For situations where the subtypes of the abstract node have different numbers of child nodes, it probably helps us generate the implementation per our conventions--calling into GetRed with the right slot and so on.

public sealed partial class FileScopedNamespaceDeclarationSyntax : BaseNamespaceDeclarationSyntax
{
private SyntaxNode? attributeLists;
private NameSyntax? name;
private SyntaxNode? externs;
private SyntaxNode? usings;
private SyntaxNode? members;
internal FileScopedNamespaceDeclarationSyntax(InternalSyntax.CSharpSyntaxNode green, SyntaxNode? parent, int position)
: base(green, parent, position)
{
}
public override SyntaxList<AttributeListSyntax> AttributeLists => new SyntaxList<AttributeListSyntax>(GetRed(ref this.attributeLists, 0));
public override SyntaxTokenList Modifiers
{
get
{
var slot = this.Green.GetSlot(1);
return slot != null ? new SyntaxTokenList(this, slot, GetChildPosition(1), GetChildIndex(1)) : default;
}
}
public override SyntaxToken NamespaceKeyword => new SyntaxToken(this, ((Syntax.InternalSyntax.FileScopedNamespaceDeclarationSyntax)this.Green).namespaceKeyword, GetChildPosition(2), GetChildIndex(2));
public override NameSyntax Name => GetRed(ref this.name, 3)!;
public SyntaxToken SemicolonToken => new SyntaxToken(this, ((Syntax.InternalSyntax.FileScopedNamespaceDeclarationSyntax)this.Green).semicolonToken, GetChildPosition(4), GetChildIndex(4));
public override SyntaxList<ExternAliasDirectiveSyntax> Externs => new SyntaxList<ExternAliasDirectiveSyntax>(GetRed(ref this.externs, 5));
public override SyntaxList<UsingDirectiveSyntax> Usings => new SyntaxList<UsingDirectiveSyntax>(GetRed(ref this.usings, 6));
public override SyntaxList<MemberDeclarationSyntax> Members => new SyntaxList<MemberDeclarationSyntax>(GetRed(ref this.members, 7));

public sealed partial class NamespaceDeclarationSyntax : BaseNamespaceDeclarationSyntax
{
private SyntaxNode? attributeLists;
private NameSyntax? name;
private SyntaxNode? externs;
private SyntaxNode? usings;
private SyntaxNode? members;
internal NamespaceDeclarationSyntax(InternalSyntax.CSharpSyntaxNode green, SyntaxNode? parent, int position)
: base(green, parent, position)
{
}
public override SyntaxList<AttributeListSyntax> AttributeLists => new SyntaxList<AttributeListSyntax>(GetRed(ref this.attributeLists, 0));
public override SyntaxTokenList Modifiers
{
get
{
var slot = this.Green.GetSlot(1);
return slot != null ? new SyntaxTokenList(this, slot, GetChildPosition(1), GetChildIndex(1)) : default;
}
}
public override SyntaxToken NamespaceKeyword => new SyntaxToken(this, ((Syntax.InternalSyntax.NamespaceDeclarationSyntax)this.Green).namespaceKeyword, GetChildPosition(2), GetChildIndex(2));
public override NameSyntax Name => GetRed(ref this.name, 3)!;
public SyntaxToken OpenBraceToken => new SyntaxToken(this, ((Syntax.InternalSyntax.NamespaceDeclarationSyntax)this.Green).openBraceToken, GetChildPosition(4), GetChildIndex(4));
public override SyntaxList<ExternAliasDirectiveSyntax> Externs => new SyntaxList<ExternAliasDirectiveSyntax>(GetRed(ref this.externs, 5));
public override SyntaxList<UsingDirectiveSyntax> Usings => new SyntaxList<UsingDirectiveSyntax>(GetRed(ref this.usings, 6));
public override SyntaxList<MemberDeclarationSyntax> Members => new SyntaxList<MemberDeclarationSyntax>(GetRed(ref this.members, 7));
public SyntaxToken CloseBraceToken => new SyntaxToken(this, ((Syntax.InternalSyntax.NamespaceDeclarationSyntax)this.Green).closeBraceToken, GetChildPosition(8), GetChildIndex(8));
/// <summary>Gets the optional semicolon token.</summary>
public SyntaxToken SemicolonToken
{
get
{
var slot = ((Syntax.InternalSyntax.NamespaceDeclarationSyntax)this.Green).semicolonToken;
return slot != null ? new SyntaxToken(this, slot, GetChildPosition(9), GetChildIndex(9)) : default;
}
}

If you ctrl+f for "abstract partial class" in the file linked above you'll find that the abstract classes don't declare fields or concrete properties, only the non-abstract subtypes do. I do not know if it would be possible to sometimes push fields into the parent type and de-virtualize some of the properties, for example, but I think it would be a new and separate thing.

@CyrusNajmabadi
Copy link
Member

Yup. This is just an affectation of how the generator works. It might be possible to change this, but there hasn't been a need yet

@RikkiGibson RikkiGibson added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jul 15, 2021
@chsienki
Copy link
Contributor

Notes

Shape / nesting

Should this really be a nesting node, or an independent node. There has been discussion about this in LDM, and the decision was made to represent it this way, so we should keep as it.

It matches the 'spirit' of the spec, so seems appropriate.

Breaking change?

This will break users of the syntax model, but either way we do it they will have to change code, so it doesn't matter either way. There is some evidence from the FXCop analyzers that handling this kind of base class extraction change is fairly painless, so we think its ok.

@taylorchasewhite
Copy link

Has this been released? In preview mode I am debugging and see FileScopeNamespaceDeclarationSyntax, but I cannot reference it or its base class in my code...

@333fred
Copy link
Member

333fred commented Aug 19, 2021

This has not been released. The issue will be closed when implemented.

@RikkiGibson
Copy link
Contributor Author

You'll have to reference a recent 4.0.0 version of the Roslyn packages to use the new types in your code.

@jaredpar
Copy link
Member

@RikkiGibson @333fred has this been addressed by API review? Getting very close to the release now and want to make sure we don't let this slip through

@333fred
Copy link
Member

333fred commented Sep 14, 2021

@RikkiGibson @333fred has this been addressed by API review? Getting very close to the release now and want to make sure we don't let this slip through

Yes, this has the api-approved label. It's ready to be implemented.

@RikkiGibson
Copy link
Contributor Author

Since this is just describing the syntax model for the feature as it was implemented, and the feature has been merged, we're all done here.

@jcouv
Copy link
Member

jcouv commented Sep 17, 2021

@RikkiGibson Can we close this issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved in API review, it can be implemented Area-Compilers blocking API needs to reviewed with priority to unblock work Concept-API This issue involves adding, removing, clarification, or modification of an API. Feature Request New Language Feature - File Scoped Namespaces
Projects
None yet
Development

No branches or pull requests

8 participants