-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Use Roslyn to parse directives in file-based programs #47978
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR integrates Roslyn APIs for parsing file-based directives and updates test expectations accordingly.
- Updated tests to expect dynamic target frameworks using ToolsetInfo.CurrentTargetFramework and refined error messages.
- Introduced new tests for directives handling (after tokens, after #if blocks, and in comments) and refactored the virtual project building logic to use Roslyn parsing.
Reviewed Changes
Copilot reviewed 2 out of 16 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs | Updates test expectations for target framework and error messages, and adds new tests for directive handling. |
| src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs | Removes directive-removal logic, updates project file generation, and incorporates Roslyn API usage for directive parsing. |
Files not reviewed (14)
- src/Cli/dotnet/Commands/Run/LocalizableStrings.resx: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.cs.xlf: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.de.xlf: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.es.xlf: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.fr.xlf: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.it.xlf: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.ja.xlf: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.ko.xlf: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.pl.xlf: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.pt-BR.xlf: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.ru.xlf: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.tr.xlf: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.zh-Hans.xlf: Language not supported
- src/Cli/dotnet/Commands/Run/xlf/LocalizableStrings.zh-Hant.xlf: Language not supported
Comments suppressed due to low confidence (2)
test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs:478
- Ensure that the updated error message wording in property directives matches the intended format and is reflected consistently in user documentation.
expectedWildcardPattern: "The property directive needs to have two parts separated by a space like 'PropertyName PropertyValue': /app/Program.cs:1");
src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs:139
- Review the removal of the _targetFilePath handling to ensure that the lack of directive removal from the source file does not lead to unintended side effects in downstream processing.
Debug.Assert(_directives.IsDefault, "{nameof(PrepareProjectInstance)} should not be called multiple times.");
|
@RikkiGibson @jaredpar @MiYanni for reviews, thanks #Resolved |
| <data name="PropertyDirectiveMissingParts" xml:space="preserve"> | ||
| <value>The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0}</value> | ||
| <comment>{0} is the file path and line number. {Locked="="}</comment> | ||
| <value>The property directive needs to have two parts separated by a space like 'PropertyName PropertyValue': {0}</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 In the previous PR, I've changed the separator to a space but forgot to change this error message. #Resolved
| #:property Name Value | ||
| #:property NugetPackageDescription "My package with spaces" | ||
| # ! /test | ||
| #! /program x |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Needed to reorder the directives in this test slightly because using Roslyn APIs means we now ignore all directives that appear after invalid directives. #Resolved
| { | ||
| VerifyConversion( | ||
| inputCSharp: """ | ||
| #:property Prop 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have tests for when the property value has characters that could impact XML processing? For example:
#:property Evil </Property>
``` #ResolvedThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, Directives_Escaping
| // Include the preceding whitespace in the span, i.e., span will be the whole line. | ||
| var span = previousWhiteSpaceSpan.IsEmpty ? trivia.FullSpan : TextSpan.FromBounds(previousWhiteSpaceSpan.Start, trivia.FullSpan.End); | ||
|
|
||
| var message = trivia.GetStructure() is IgnoredDirectiveTriviaSyntax { EndOfDirectiveToken.LeadingTrivia: [{ RawKind: (int)SyntaxKind.PreprocessingMessageTrivia } messageTrivia] } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Man, I need to get better at reading/writing using pattern matching syntax. ☠️ #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's very powerful, we use it in roslyn a lot, but I guess it can seem also quite opaque at first and maybe I'm overusing it :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naw, it just has a learning curve. I like simplifying imperative logic, so it is definitely something I want to get used to using!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API review decision will cause this to change to use a more straightforward Message prop access, right? We won't ship with the message accessible in this way on this particular trivia?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API review decision will cause this to change to use a more straightforward
Messageprop access, right?
Yes, once dotnet/roslyn#77968 is merged and flows here, this will be simplified as you say.
|
Taking a look #Resolved |
| expectedCSharp: """ | ||
| #define X | ||
| Console.WriteLine(); | ||
| #:property Prop 3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be reasonable for the convert invocation to fail in the presence of this directive? Essentially make the user get their directives in shape before running 'convert' so that we don't leave them in an in-between state, with a directive that is never going to work anyway, and which they now have to figure out how to move over to their project cleanly. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good idea, I'm going to implement that, thanks.
| _directives = FindDirectives(sourceFile); | ||
|
|
||
| // If there were any `#:` directives, remove them from the file. | ||
| // (This is temporary until Roslyn is updated to ignore them.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty much the same logic is sticking around for convert right? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
| foreach (var trivia in result.Token.LeadingTrivia) | ||
| { | ||
| var lineText = sourceFile.Text.ToString(line.Span); | ||
| // Stop when the trivia contains an error (e.g., because it's after #if). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should be more specific about what kinds of errors make us stop. Essentially if something happens to be malformed is doing that likely to prevent a cascade or produce a result which is more actionable. Perhaps we can simply adjust if we find that to be the case. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
During 'dotnet run file.cs', compiler reports errors that I think are actionable. For 'dotnet project convert', addressing your other comment should start producing more actionable errors there as well.
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am curious what happens when user writes #:property TargetFramework net9.0 or #:property TargetFramework net472 or what have you. Or, heck, #:property OutputType Library. No need to test it here and now but perhaps eventually to demonstrate the behavior. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a test for this - it's named Directives, see below. The user values will be added to a PropertyGroup below this one, so MSBuild will deal with them and overwrite the built-ins, see also #47702 (comment).
sdk/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs
Lines 319 to 359 in 5086618
| [Fact] | |
| public void Directives() | |
| { | |
| VerifyConversion( | |
| inputCSharp: """ | |
| #!/program | |
| #:sdk Microsoft.NET.Sdk | |
| #:sdk Aspire.Hosting.Sdk 9.1.0 | |
| #:property TargetFramework net11.0 | |
| #:package System.CommandLine 2.0.0-beta4.22272.1 | |
| #:property LangVersion preview | |
| Console.WriteLine(); | |
| """, | |
| expectedProject: $""" | |
| <Project Sdk="Microsoft.NET.Sdk"> | |
| <Sdk Name="Aspire.Hosting.Sdk" Version="9.1.0" /> | |
| <PropertyGroup> | |
| <OutputType>Exe</OutputType> | |
| <TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework> | |
| <ImplicitUsings>enable</ImplicitUsings> | |
| <Nullable>enable</Nullable> | |
| </PropertyGroup> | |
| <PropertyGroup> | |
| <TargetFramework>net11.0</TargetFramework> | |
| <LangVersion>preview</LangVersion> | |
| </PropertyGroup> | |
| <ItemGroup> | |
| <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> | |
| </ItemGroup> | |
| </Project> | |
| """, | |
| expectedCSharp: """ | |
| Console.WriteLine(); | |
| """); | |
| } |
|
|
||
| if (trivia.IsKind(SyntaxKind.ShebangDirectiveTrivia)) | ||
| { | ||
| Debug.Assert(previousWhiteSpaceSpan.IsEmpty, "#! should be at the first character"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think this is something we can assert; just skip it or otherwise produce an error. Asserts should be for invariants, not potential bad syntax from a user. Please make sure we have tests that would trigger this assert. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we can trigger this assert. Roslyn doesn't produce ShebangDirectiveTrivia if it's not at the first character, see the hashPosition == 0 condition in https://github.com/dotnet/roslyn/blob/256273c38fd5efffa8a18b921f66b281176b02c1/src/Compilers/CSharp/Portable/Parser/DirectiveParser.cs#L111-L114.
That being said, we have tests here in sdk that have a shebang directive with some whitespace before, so if parsing of that changes in Roslyn, this assert would be hit.
So, I would consider this to be an invariant and if it changes, the assert will catch it, and we might need to react somehow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we can trigger this assert. Roslyn doesn't produce ShebangDirectiveTrivia if it's not at the first character, see the hashPosition == 0 condition in https://github.com/dotnet/roslyn/blob/256273c38fd5efffa8a18b921f66b281176b02c1/src/Compilers/CSharp/Portable/Parser/DirectiveParser.cs#L111-L114
This seems like a bug, not a feature. I expect that to be fixed at some point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I will remove the assert, thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed dotnet/roslyn#78054 to follow up on this.
Uses APIs from dotnet/roslyn#77696.