1515using Microsoft . Build . Framework ;
1616using Microsoft . Build . Logging ;
1717using Microsoft . CodeAnalysis ;
18+ using Microsoft . CodeAnalysis . CSharp ;
19+ using Microsoft . CodeAnalysis . CSharp . Syntax ;
1820using Microsoft . CodeAnalysis . Text ;
1921using Microsoft . DotNet . Cli . Utils ;
2022
@@ -26,7 +28,6 @@ namespace Microsoft.DotNet.Cli.Commands.Run;
2628internal sealed class VirtualProjectBuildingCommand
2729{
2830 private ImmutableArray < CSharpDirective > _directives ;
29- private string ? _targetFilePath ;
3031
3132 public Dictionary < string , string > GlobalProperties { get ; } = new ( StringComparer . OrdinalIgnoreCase ) ;
3233 public required string EntryPointFileFullPath { get ; init ; }
@@ -132,22 +133,10 @@ public int Execute(string[] binaryLoggerArgs, ILogger consoleLogger)
132133 /// </summary>
133134 public VirtualProjectBuildingCommand PrepareProjectInstance ( )
134135 {
135- Debug . Assert ( _directives . IsDefault && _targetFilePath is null , $ "{ nameof ( PrepareProjectInstance ) } should not be called multiple times.") ;
136+ Debug . Assert ( _directives . IsDefault , $ "{ nameof ( PrepareProjectInstance ) } should not be called multiple times.") ;
136137
137138 var sourceFile = LoadSourceFile ( EntryPointFileFullPath ) ;
138- _directives = FindDirectives ( sourceFile ) ;
139-
140- // If there were any `#:` directives, remove them from the file.
141- // (This is temporary until Roslyn is updated to ignore them.)
142- _targetFilePath = EntryPointFileFullPath ;
143- if ( _directives . Length != 0 )
144- {
145- var targetDirectory = Path . Join ( Path . GetDirectoryName ( _targetFilePath ) , "obj" ) ;
146- Directory . CreateDirectory ( targetDirectory ) ;
147- _targetFilePath = Path . Join ( targetDirectory , Path . GetFileName ( _targetFilePath ) ) ;
148-
149- RemoveDirectivesFromFile ( _directives , sourceFile . Text , _targetFilePath ) ;
150- }
139+ _directives = FindDirectives ( sourceFile , reportErrors : false ) ;
151140
152141 return this ;
153142 }
@@ -177,15 +166,15 @@ private ProjectInstance CreateProjectInstance(
177166
178167 ProjectRootElement CreateProjectRootElement ( ProjectCollection projectCollection )
179168 {
180- Debug . Assert ( ! _directives . IsDefault && _targetFilePath is not null , $ "{ nameof ( PrepareProjectInstance ) } should have been called first.") ;
169+ Debug . Assert ( ! _directives . IsDefault , $ "{ nameof ( PrepareProjectInstance ) } should have been called first.") ;
181170
182171 var projectFileFullPath = Path . ChangeExtension ( EntryPointFileFullPath , ".csproj" ) ;
183172 var projectFileWriter = new StringWriter ( ) ;
184173 WriteProjectFile (
185174 projectFileWriter ,
186175 _directives ,
187176 isVirtualProject : true ,
188- targetFilePath : _targetFilePath ,
177+ targetFilePath : EntryPointFileFullPath ,
189178 artifactsPath : GetArtifactsPath ( EntryPointFileFullPath ) ) ;
190179 var projectFileText = projectFileWriter . ToString ( ) ;
191180
@@ -330,6 +319,17 @@ private static void WriteProjectFile(
330319 writer . WriteLine ( " </PropertyGroup>" ) ;
331320 }
332321
322+ if ( isVirtualProject )
323+ {
324+ // After `#:property` directives so they don't override this.
325+ writer . WriteLine ( """
326+
327+ <PropertyGroup>
328+ <Features>$(Features);FileBasedProgram</Features>
329+ </PropertyGroup>
330+ """ ) ;
331+ }
332+
333333 if ( packageDirectives . Any ( ) )
334334 {
335335 writer . WriteLine ( """
@@ -426,28 +426,101 @@ Override targets which don't work with project files that are not present on dis
426426 static string EscapeValue ( string value ) => SecurityElement . Escape ( value ) ;
427427 }
428428
429- public static ImmutableArray < CSharpDirective > FindDirectives ( SourceFile sourceFile )
429+ public static ImmutableArray < CSharpDirective > FindDirectivesForConversion ( SourceFile sourceFile , bool force )
430+ {
431+ return FindDirectives ( sourceFile , reportErrors : ! force ) ;
432+ }
433+
434+ #pragma warning disable RSEXPERIMENTAL003 // 'SyntaxTokenParser' is experimental
435+ #pragma warning disable RSEXPERIMENTAL005 // 'IgnoredDirectiveTriviaSyntax' is experimental
436+ private static ImmutableArray < CSharpDirective > FindDirectives ( SourceFile sourceFile , bool reportErrors )
430437 {
431438 var builder = ImmutableArray . CreateBuilder < CSharpDirective > ( ) ;
439+ SyntaxTokenParser tokenizer = SyntaxFactory . CreateTokenParser ( sourceFile . Text ,
440+ CSharpParseOptions . Default . WithFeatures ( [ new ( "FileBasedProgram" , "true" ) ] ) ) ;
432441
433- // NOTE: When Roslyn is updated to support "ignored directives", we should use its SyntaxTokenParser instead.
434- foreach ( var line in sourceFile . Text . Lines )
442+ var result = tokenizer . ParseLeadingTrivia ( ) ;
443+ TextSpan previousWhiteSpaceSpan = default ;
444+ foreach ( var trivia in result . Token . LeadingTrivia )
435445 {
436- var lineText = sourceFile . Text . ToString ( line . Span ) ;
446+ // Stop when the trivia contains an error (e.g., because it's after #if).
447+ if ( trivia . ContainsDiagnostics )
448+ {
449+ break ;
450+ }
451+
452+ if ( trivia . IsKind ( SyntaxKind . WhitespaceTrivia ) )
453+ {
454+ Debug . Assert ( previousWhiteSpaceSpan . IsEmpty ) ;
455+ previousWhiteSpaceSpan = trivia . FullSpan ;
456+ continue ;
457+ }
458+
459+ if ( trivia . IsKind ( SyntaxKind . ShebangDirectiveTrivia ) )
460+ {
461+ TextSpan span = getFullSpan ( previousWhiteSpaceSpan , trivia ) ;
437462
438- if ( Patterns . Shebang . IsMatch ( lineText ) )
463+ builder . Add ( new CSharpDirective . Shebang { Span = span } ) ;
464+ }
465+ else if ( trivia . IsKind ( SyntaxKind . IgnoredDirectiveTrivia ) )
439466 {
440- builder . Add ( new CSharpDirective . Shebang { Span = line . SpanIncludingLineBreak } ) ;
467+ TextSpan span = getFullSpan ( previousWhiteSpaceSpan , trivia ) ;
468+
469+ var message = trivia . GetStructure ( ) is IgnoredDirectiveTriviaSyntax { EndOfDirectiveToken . LeadingTrivia : [ { RawKind : ( int ) SyntaxKind . PreprocessingMessageTrivia } messageTrivia ] }
470+ ? messageTrivia . ToString ( ) . AsSpan ( ) . Trim ( )
471+ : "" ;
472+ var parts = Patterns . Whitespace . EnumerateSplits ( message , 2 ) ;
473+ var name = parts . MoveNext ( ) ? message [ parts . Current ] : default ;
474+ var value = parts . MoveNext ( ) ? message [ parts . Current ] : default ;
475+ Debug . Assert ( ! parts . MoveNext ( ) ) ;
476+ builder . Add ( CSharpDirective . Parse ( sourceFile , span , name . ToString ( ) , value . ToString ( ) ) ) ;
441477 }
442- else if ( Patterns . Directive . Match ( lineText ) is { Success : true } match )
478+
479+ previousWhiteSpaceSpan = default ;
480+ }
481+
482+ // In conversion mode, we want to report errors for any invalid directives in the rest of the file
483+ // so users don't end up with invalid directives in the converted project.
484+ if ( reportErrors )
485+ {
486+ tokenizer . ResetTo ( result ) ;
487+
488+ do
443489 {
444- builder . Add ( CSharpDirective . Parse ( sourceFile , line . SpanIncludingLineBreak , match . Groups [ 1 ] . Value , match . Groups [ 2 ] . Value ) ) ;
490+ result = tokenizer . ParseNextToken ( ) ;
491+
492+ foreach ( var trivia in result . Token . LeadingTrivia )
493+ {
494+ reportErrorFor ( sourceFile , trivia ) ;
495+ }
496+
497+ foreach ( var trivia in result . Token . TrailingTrivia )
498+ {
499+ reportErrorFor ( sourceFile , trivia ) ;
500+ }
445501 }
502+ while ( ! result . Token . IsKind ( SyntaxKind . EndOfFileToken ) ) ;
446503 }
447504
448505 // The result should be ordered by source location, RemoveDirectivesFromFile depends on that.
449506 return builder . ToImmutable ( ) ;
507+
508+ static TextSpan getFullSpan ( TextSpan previousWhiteSpaceSpan , SyntaxTrivia trivia )
509+ {
510+ // Include the preceding whitespace in the span, i.e., span will be the whole line.
511+ return previousWhiteSpaceSpan . IsEmpty ? trivia . FullSpan : TextSpan . FromBounds ( previousWhiteSpaceSpan . Start , trivia . FullSpan . End ) ;
512+ }
513+
514+ static void reportErrorFor ( SourceFile sourceFile , SyntaxTrivia trivia )
515+ {
516+ if ( trivia . ContainsDiagnostics && trivia . IsKind ( SyntaxKind . IgnoredDirectiveTrivia ) )
517+ {
518+ throw new GracefulException ( CliCommandStrings . CannotConvertDirective , sourceFile . GetLocationString ( trivia . Span ) ) ;
519+ }
520+ }
450521 }
522+ #pragma warning restore RSEXPERIMENTAL005 // 'IgnoredDirectiveTriviaSyntax' is experimental
523+ #pragma warning restore RSEXPERIMENTAL003 // 'SyntaxTokenParser' is experimental
451524
452525 public static SourceFile LoadSourceFile ( string filePath )
453526 {
@@ -500,11 +573,8 @@ public string GetLocationString(TextSpan span)
500573
501574internal static partial class Patterns
502575{
503- [ GeneratedRegex ( """^\s*#:\s*(\w*)\s*(.*?)\s*$""" ) ]
504- public static partial Regex Directive { get ; }
505-
506- [ GeneratedRegex ( """^\s*#!.*$""" ) ]
507- public static partial Regex Shebang { get ; }
576+ [ GeneratedRegex ( """\s+""" ) ]
577+ public static partial Regex Whitespace { get ; }
508578}
509579
510580/// <summary>
0 commit comments