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

Draft implementation of file-scoped-namespaces #48937

Conversation

CyrusNajmabadi
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi commented Oct 27, 2020

Draft impl of championed feature: dotnet/csharplang#137
Speclet at: dotnet/csharplang#4073
Test plan: #49000

To help ground discussion, find issues, and garner feedback to help with the language design process.

The general approach here was to take an approach similar to what we did with TypeScript. Specifically, given the two namespaces:

namespace X; class C { } and
namespace X { class C { } }

We represent these with different concrete types, but a similar abstract parse tree shape. Specifically, both generate a parse tree like so:

CompilationUnit
|
NamespaceDeclaration (or SingleLineNamespaceDeclaration)
|
-- Name
|    |
|    -- X
-- Members
    |
    -- ClassDeclaration
        |
       -- Name
            |
            -- C

NamespaceDeclaration and the new SingleLineNamespaceDeclaration derive from a common BaseNamespaceDeclaration. This type contains the Name, Externs, Usings, Attributes and Members. The two subclasses contain the only syntactic differents (the curlies, or hte semicolon).

By doing this, we allow all code to still presume a certain containment structure for files. For example, in the IDE there is lots of code that will walk upwards in a file looking for the containing namespace declaration. This approach allows all existing code (that doesn't care about curlies) to just move to processing 'BaseNamespaceDeclaration's, isntead of 'NamespaceDeclaration's, and allows anything that currently looks for SyntaxKind.NamespaceDeclaration to just look for that and SyntaxKind.SingleLineNamespaceDeclaration.

--

For my testing approach, i made targeted parsing tests and targeted tests for the specific specialized errors we produce. I then searched the compiler test base for usages of NamespaceDeclarationSyntax, SyntaxKind.NamespaceDeclaration, INamespaceSymbol, and NamespaceSymbol and generally created sibling tests that validated the same behavior when a single-line namespace was used instead.

@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner October 27, 2020 01:17
@CyrusNajmabadi CyrusNajmabadi marked this pull request as draft October 27, 2020 01:17
@jcouv

This comment has been minimized.

@jaredpar

This comment has been minimized.

@CyrusNajmabadi

This comment has been minimized.

@CyrusNajmabadi

This comment has been minimized.

@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review February 24, 2021 18:29
@jcouv jcouv requested a review from chsienki February 24, 2021 18:48
@jaredpar

This comment has been minimized.

@chsienki chsienki changed the base branch from master to features/FileScopedNamespaces February 26, 2021 00:56
@chsienki

This comment has been minimized.

@jcouv jcouv added this to the C# 10 milestone Mar 23, 2021
@@ -4470,6 +4510,7 @@ private void ParseVariableDeclarators(TypeSyntax type, VariableFlags flags, Sepa
// the reported errors should take into consideration whether or not one expects them in the current context.
bool variableDeclarationsExpected =
parentKind != SyntaxKind.NamespaceDeclaration &&
parentKind != SyntaxKind.SingleLineNamespaceDeclaration &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a test for this change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RikkiGibson to ensure we've addressed this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the tests.

var semicolon = this.TryEatToken(SyntaxKind.SemicolonToken);
SyntaxListBuilder initialBadNodes = null;
this.ParseNamespaceBody(ref semicolon, ref body, ref initialBadNodes, SyntaxKind.SingleLineNamespaceDeclaration);
Debug.Assert(initialBadNodes == null); // init bad nodes should have been attached to open brace...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment is wrong (would be attached to semicolon). Do we have a test scenario where that happens?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RikkiGibson to address

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See SingleLineDeclarationParsingTests.NamespaceWithExtraSemicolon. Will fix the comment.

var comp = CreateCompilation(tree);
var model = comp.GetSemanticModel(tree);
var errors = comp.GetDiagnostics().ToArray();
Assert.Equal(2, errors.Length);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Let's check the diagnostics instead of just the count

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am OK with this because it's taken from the test directly above.

Assert.False(classDecl.Identifier.IsMissing);
Assert.True(classDecl.OpenBraceToken.IsMissing);
Assert.True(classDecl.CloseBraceToken.IsMissing);
var ns = root.DescendantNodes().OfType<SingleLineNamespaceDeclarationSyntax>().Single();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you intend to check something on ns?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RikkiGibson to address

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not that meaningful but I'll add an assert that the semicolon is present.

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM Thanks (iteration 26) with a couple test suggestions

@jcouv jcouv self-assigned this Mar 25, 2021
{
diagnostics.Add(ErrorCode.ERR_SingleLineAndNormalNamespace, node.Name.GetLocation());
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also have a check to disallow top-level statements.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RikkiGibson Add PROTOTYPE or track elsewehere?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is handled by the existing mechanisms that prevent top-level statements within namespaces. See SingleLineNamespaceWithFollowingStatement in AssemblyAndNamespaceTests.cs

Copy link
Contributor

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. My expectation is I will make the changes to resolve the comments myself in a follow-up PR.

@@ -83,9 +83,9 @@ internal string GetDebuggerDisplay()
usingDirectives = inUsing ? default(SyntaxList<UsingDirectiveSyntax>) : compilationUnit.Usings;
externAliasDirectives = compilationUnit.Externs;
}
else if (declarationSyntax.Kind() == SyntaxKind.NamespaceDeclaration)
else if (declarationSyntax.Kind() is SyntaxKind.NamespaceDeclaration or SyntaxKind.SingleLineNamespaceDeclaration)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if we should have an extension for this. Depends on how often this check is performed of course.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RikkiGibson I find 5 or 6 instances of this check so an extension sounds fine. I'd say push a commit if you'd like (you're in charge of the PR ;-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really interested in doing this at this time. Would rather make the adjustment as part of a larger refactoring e.g. to replace Kind() tests with type tests in more places.

@@ -2266,7 +2268,8 @@ private NamespaceOrTypeSymbol GetDeclaredTypeMemberContainer(CSharpSyntaxNode me
if (memberDeclaration.Parent.Kind() == SyntaxKind.CompilationUnit)
{
// top-level namespace:
if (memberDeclaration.Kind() == SyntaxKind.NamespaceDeclaration)
if (memberDeclaration.Kind() == SyntaxKind.NamespaceDeclaration ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: feels like we should use or here instead of calling .Kind() twice

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -2297,7 +2300,8 @@ private NamespaceOrTypeSymbol GetDeclaredTypeMemberContainer(CSharpSyntaxNode me
}

// a namespace or a type in an explicitly declared namespace:
if (memberDeclaration.Kind() == SyntaxKind.NamespaceDeclaration || SyntaxFacts.IsTypeDeclaration(memberDeclaration.Kind()))
if (memberDeclaration.Kind() is SyntaxKind.NamespaceDeclaration or SyntaxKind.SingleLineNamespaceDeclaration ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: prefer binary operators on the start of the next line. For example || SyntaxFacts.IsTypeDeclaration(...)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -4468,6 +4495,99 @@ class C
SymbolDisplayPartKind.EventName);
}

[WorkItem(791756, "http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/791756")]
[Fact]
public void KindOptionsSingleLineNamespace()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may end up coming up with some kind of helper that makes it easier to express these tests as [Theory].

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have done this for many of the tests.

@@ -421,28 +464,27 @@ namespace System {}
}

[Fact]
public void TypeParameterDeclaringSyntax()
public void TypeParameterDeclaringSyntaxSingleLineNamespace()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand why the "regular" namespace version of the test was essentially removed here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Let's add the original back, or make this a Theory to cover both

@@ -471,5 +625,166 @@ namespace N { }
// (2,1): error CS1671: A namespace declaration cannot have modifiers or attributes
Diagnostic(ErrorCode.ERR_BadModifiersOnNamespace, "[System.Obsolete]").WithLocation(1, 1));
}

private static readonly CSharpParseOptions s_previewOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Typically we use TestOptions.RegularPreview

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed this. I think it could be handy to be able to change this from Preview to Regular in one place. We have done this for other features in the past, IIRC.

@RikkiGibson
Copy link
Contributor

@jcouv @chsienki My expectation is that we will try to merge this PR more or less as-is, then submit a follow up which resolves any unresolved comments on this PR. Does that seem reasonable? Or should I maybe just push commits to this PR?

@jaredpar
Copy link
Member

@RikkiGibson That seems reasonable to me. This PR is already quite large, a bit unweildy (evidenced by the current conflicts), and we are going into a feature branch. I would focus on getting this merged then follow up with a PR that cleans up the items that you noticed.

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM Thanks (iteration 28)

@RikkiGibson RikkiGibson merged commit 1a69700 into dotnet:features/FileScopedNamespaces Jun 21, 2021
@CyrusNajmabadi CyrusNajmabadi deleted the singleLineNamespaces branch July 1, 2022 16:24
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.

7 participants