diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index 6a0e393daefd..df24463621f7 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -58,8 +58,12 @@ Additionally, the implicit project file has the following customizations: in case there is a project or solution in the same directory as the file-based app. This ensures that items from nested projects and artifacts are not included by the app. + - `EnableDefaultCompileItems` property is set to `false` and there is a special `Compile` item for the entry-point file instead. + See [multiple files](#multiple-files) for more details. + - `EnableDefaultEmbeddedResourceItems` and `EnableDefaultNoneItems` properties are set to `false` if the default SDK (`Microsoft.NET.Sdk`) is being used. This avoids including files like `./**/*.resx` in simple file-based apps where users usually don't expect that. + See [multiple files](#multiple-files) for more details. ## Grow up @@ -72,16 +76,13 @@ This action should not change the behavior of the target program. dotnet project convert file.cs ``` -The command takes a path which can be either -- path to the entry-point file in case of single entry-point programs, or -- path to the target directory (then all entry points are converted; - it is not possible to convert just a single entry point in multi-entry-point program). +The command takes a path to the entry-point file. ## Target path The path passed to `dotnet run ./some/path.cs` is called *the target path*. -The target path must be a file which either has the `.cs` file extension, -or a file whose contents start with `#!`. +The target path must point to *the entry-point file* which +either has the `.cs` file extension, or whose contents start with `#!`. *The target directory* is the directory of the target file. ## Integration into the existing `dotnet run` command @@ -128,81 +129,24 @@ Command `dotnet clean file.cs` can be used to clean build artifacts of the file- Commands `dotnet package add PackageName --file app.cs` and `dotnet package remove PackageName --file app.cs` can be used to manipulate `#:package` directives in the C# files, similarly to what the commands do for project-based apps. -## Entry points - -If a file is given to `dotnet run`, it has to be an *entry-point file*, otherwise an error is reported. -We want to report an error for non-entry-point files to avoid the confusion of being able to `dotnet run util.cs`. - -Internally, the SDK CLI detects entry points by parsing all `.cs` files in the directory tree of the entry point file with default parsing options (in particular, no ``) -and checking which ones contain top-level statements (`Main` methods are not supported for now as that would require full semantic analysis, not just parsing). -Results of this detection are used to exclude other entry points from [builds](#multiple-entry-points) and [file-level directive collection](#directives-for-project-metadata). -This means the CLI might consider a file to be an entry point which later the compiler doesn't -(for example because its top-level statements are under `#if !SYMBOL` and the build has `DefineConstants=SYMBOL`). -However such inconsistencies should be rare and hence that is a better trade off than letting the compiler decide which files are entry points -because that could require multiple builds (first determine entry points and then re-build with file-level directives except those from other entry points). -To avoid parsing all C# files twice (in CLI and in the compiler), the CLI could use the compiler server for parsing so the trees are reused -(unless the parse options change via the directives), and also [cache](#optimizations) the results to avoid parsing on subsequent runs. - -## Multiple C# files +## Multiple files -Because of the [implicit project file](#implicit-project-file), -other files in the target directory or its subdirectories are included in the compilation. -For example, other `.cs` files but also `.resx` (embedded resources). -Similarly, implicit build files like `Directory.Build.props` or `Directory.Packages.props` are used during the build. +By default, the app only includes the entry-point file (plus any implicit build files like `Directory.Build.props` are considered). +This is achieved by having property `EnableDefaultCompileItems=false` and entry-point file `Compile` item in the virtual project. +(The `Compile` item has `Exclude="@(Compile)"` to avoid duplicate `Compile` items when using e.g., `#:include *.cs` directive). -> [!CAUTION] -> Multi-file support is postponed for .NET 11. -> In .NET 10, only the single file passed as the command-line argument to `dotnet run` is part of the compilation. -> Specifically, the virtual project has property `EnableDefaultCompileItems=false` -> (which can be customized via `#:property` directive), and a `Compile` item for the entry point file. -> During [conversion](#grow-up), any `Content`, `None`, `Compile`, and `EmbeddedResource` items that do not have metadata `ExcludeFromFileBasedAppConversion=true` -> and that are files inside the entry point file's directory tree are copied to the converted directory. - -### Nested files - -If there are nested project files like -``` -App/File.cs -App/Nested/Nested.csproj -App/Nested/File.cs -``` -executing `dotnet run app/file.cs` includes the nested `.cs` file in the compilation. -That is consistent with normal builds with explicit project files -and usually the build fails because there are multiple entry points or other clashes. +Thanks to this, it is possible to have multiple file-based apps in a single directory. -For `.csproj` files inside the target directory and its parent directories, we do not report any errors/warnings. -That's because it might be perfectly reasonable to have file-based programs nested in another project-based program -(most likely excluded from that project's compilation via something like ``). - -### Multiple entry points - -If there are multiple entry-point files in the target directory, the build ignores other entry-point files. -It is an error to have an entry-point file in a subdirectory of the target directory -(because it is unclear how such program should be converted to a project-based one). - -Thanks to this, it is possible to have a structure like -``` -App/Util.cs -App/Program1.cs -App/Program2.cs -``` -where either `Program1.cs` or `Program2.cs` can be run and both of them have access to `Util.cs`. - -Behind the scenes, there are multiple implicit projects -(and during [grow up](#grow-up), multiple project files are materialized -and the original C# files are moved to the corresponding project subdirectories): -``` -App/Shared/Util.cs -App/Program1/Program1.cs -App/Program1/Program1.csproj -App/Program2/Program2.cs -App/Program2/Program2.csproj -``` +Default items like `.resx` are included in the build only if a non-default `#:sdk` is used (to improve performance for simple file-based apps). +Again, this is achieved by setting `EnableDefaultNoneItems=false` and `EnableDefaultEmbeddedResourceItems=false` in the virtual project. -The generated folders might need to be named differently to avoid clashes with existing folders. +To customize this default behavior, you can use the `#:include`/`#:exclude` [directives](#directives-for-project-metadata) +or use some custom MSBuild code like `#:property EnableDefaultCompileItems=true`. -The entry-point projects (`Program1` and `Program2` in our example) -have the shared `.cs` files source-included via ``. +During [conversion](#grow-up), any `Content`, `None`, `Compile`, and `EmbeddedResource` items that do not have metadata `ExcludeFromFileBasedAppConversion=true` are copied into the output directory. +The implicit `EnableDefault*Items` properties and `Compile` item are not preserved in the converted project, +because the constructed project will have the same behavior even if all default items are included thanks to the output directory being isolated +and the conversion process only copying the items that were included in the original virtual project. ## Build outputs @@ -223,9 +167,6 @@ The same cleanup can be performed manually via command `dotnet clean-file-based- It is possible to specify some project metadata via *file-level directives* which are [ignored][ignored-directives] by the C# language but recognized by the SDK CLI. -Directives `sdk`, `package`, `property`, and `project` are translated into -``, ``, ``, and `` project elements, respectively. -Other directives result in an error, reserving them for future use. ```cs #:sdk Microsoft.NET.Sdk.Web @@ -233,12 +174,13 @@ Other directives result in an error, reserving them for future use. #:property LangVersion=preview #:package System.CommandLine@2.0.0-* #:project ../MyLibrary +#:include ./**/*.cs ``` Each directive has a kind (e.g., `package`), a name (e.g., `System.CommandLine`), a separator (e.g., `@`), and a value (e.g., the package version). The value is required for `#:property`, optional for `#:package`/`#:sdk`, and disallowed for `#:project`. -The name must be separated from the kind (`package`/`sdk`/`property`) of the directive by whitespace +The name must be separated from the kind of the directive by whitespace and any leading and trailing white space is not considered part of the name and value. The directives are processed as follows: @@ -247,17 +189,39 @@ The directives are processed as follows: and the subsequent `#:sdk` directive names and values are injected as `` elements (or without the `Version` attribute if it has no value). It is an error if the name is empty (the version is allowed to be empty, but that results in empty `Version=""`). -- A `#:property` is injected as `<{0}>{1}` in a ``. +- Each `#:property` is injected as `<{0}>{1}` in a ``. It is an error if property does not have a value or if its name is empty (the value is allowed to be empty) or contains invalid characters. -- A `#:package` is injected as `` (or without the `Version` attribute if it has no value) in an ``. +- Each `#:package` is injected as `` (or without the `Version` attribute if it has no value) in an ``. It is an error if its name is empty (the value, i.e., package version, is allowed to be empty, but that results in empty `Version=""`). -- A `#:project` is injected as `` in an ``. + It is valid to have a `#:package` directive without a version. + That's useful when central package management (CPM) is used. + NuGet will report an appropriate error if the version is missing and CPM is not enabled. + +- Each `#:project` is injected as `` in an ``. + It is an error if the value is empty. If the path points to an existing directory, a project file is found inside that directory and its path is used instead (because `ProjectReference` items don't support directory paths). An error is reported if zero or more than one projects are found in the directory, just like `dotnet reference add` would do. +- Each `#:include` is injected as `<{1} Include="{0}" />` in an `` + where `{0}` is the directive's value and `{1}` is determined by its extension: + + - `.cs` → `Compile` + - `.resx` → `EmbeddedResource` + - `.json` or `.razor` → `Content` + - Other extensions currently result in an error. We might support more in the future + or introduce an extensibility system where users can specify the mapping. + + It is an error if the value is empty. + + Relative paths are resolved relative to the file containing the directive. + +- Each `#:exclude` is injected similarly to `#:include` but with `Remove="{0}"` instead of `Include="{0}"`. + +- Other directive kinds result in an error, reserving them for future use. + Directive values support MSBuild variables (like `$(..)`) normally as they are translated literally and left to MSBuild engine to process. However, in `#:project` directives, variables might not be preserved during [grow up](#grow-up), because there is additional processing of those directives that makes it technically challenging to preserve variables in all cases @@ -285,11 +249,10 @@ Later with deduplication, separate "self-contained" utilities could reference ov even if they end up in the same compilation. For example, properties could be concatenated via `;`, more specific package versions could override less specific ones. -It is valid to have a `#:package` directive without a version. -That's useful when central package management (CPM) is used. -NuGet will report an appropriate error if the version is missing and CPM is not enabled. +Directives are processed from all `.cs` files included in the compilation +(no matter whether the `Compile` items are specified in some MSBuild code or inferred from `#:include`). -During [grow up](#grow-up), `#:` directives are removed from the `.cs` files and turned into elements in the corresponding `.csproj` files. +During [grow up](#grow-up), `#:` directives are removed from the `.cs` files and turned into elements in the converted `.csproj` file. For project-based programs, `#:` directives are an error (reported by Roslyn when it's told it is in "project-based" mode). `#!` directives are also removed during grow up, although we could consider to have an option to preserve them (since they might still be valid after grow up, depending on which program they are actually specifying to "interpret" the file, i.e., it might not be `dotnet run` at all). @@ -346,40 +309,18 @@ would need to search for a file-based program in the current directory instead o We could add a universal option that works with both project-based and file-based programs, like `dotnet run --directory ./dir/`. For inspiration, `dotnet test` also has a `--directory` option. We already have a `--file` option. Both could be unified as `--path`. - -If we want to also support [multi-entry-point scenarios](#multiple-entry-points), -we might need an option like `dotnet run --entry ./dir/name` which would work for both `./dir/name.cs` and `./dir/name/name.csproj`. +Since there can be multiple entry points in a single directory, it would be useful to have +an option like `dotnet run --entry ./dir/name` which would work for both `./dir/name.cs` and `./dir/name/name.csproj`. ### Nested files errors -Performance issues might arise if there are many [nested files](#nested-files) (possibly unintentionally), -and it might not be clear to users that `dotnet run file.cs` will include other `.cs` files in the compilation. -Therefore, we could consider some switch (a command-line option and/or a `#` language directive) to enable/disable this behavior. -When disabled, [grow up](#grow-up) would generate projects in subdirectories -similarly to [multi-entry-point scenarios](#multiple-entry-points) to preserve the program's behavior. - -Including `.cs` files from nested folders which contain `.csproj`s might be unexpected, -hence we could consider excluding items from nested project folders. - -Similarly, we could report an error if there are many nested directories and files, -so for example, if someone puts a C# file into `C:/sources` and executes `dotnet run C:/sources/file.cs` or opens that in the IDE, -we do not walk all user's sources. Again, this problem exists with project-based programs as well. -Note that having a project-based or file-based program in the drive root would result in +We could report an error if there are many nested directories and files, +so for example, if someone puts a C# file with `#:include **.cs` into `C:/sources` +and executes `dotnet run C:/sources/file.cs` or opens that in the IDE, +we do not walk all user's sources. Note that this problem exists with project-based programs as well. +Also, having a project-based or file-based program in the drive root would result in [error MSB5029](https://learn.microsoft.com/visualstudio/msbuild/errors/msb5029). -### Multiple entry points implementation - -We could consider using `InternalsVisibleTo` attribute but that might result in slight differences between single- and multi-entry-point programs -(if not now then perhaps in the future if [some "more internal" accessibility](https://github.com/dotnet/csharplang/issues/6794) is added to C# which doesn't respect `InternalsVisibleTo`) -which would be undesirable when users start with a single entry point and later add another. -Also, `InternalsVisibleTo` needs to be added into a C# file as an attribute, or via a complex-looking `AssemblyAttribute` item group into the `.csproj` like: - -```xml - - - -``` - ### Shebang support Some shells do not support multiple command-line arguments in the shebang diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx index 0af28bb5fd1f..18736fcb61c2 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx @@ -1,17 +1,17 @@  - @@ -134,6 +134,10 @@ The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. {Locked="#:property"} + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' + Static graph restore is not supported for file-based apps. Remove the '#:property'. {Locked="#:property"} @@ -169,7 +173,4 @@ Unrecognized directive '{0}'. {0} is the directive name like 'package' or 'sdk'. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs index 8457183e2cba..a0d73f616ca4 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs @@ -117,6 +117,7 @@ public static void FindLeadingDirectives( var whiteSpace = GetWhiteSpaceInfo(triviaList, index); var info = new CSharpDirective.ParseInfo { + SourceFile = sourceFile, Span = span, LeadingWhiteSpace = whiteSpace.Leading, TrailingWhiteSpace = whiteSpace.Trailing, @@ -140,12 +141,12 @@ public static void FindLeadingDirectives( { Info = new() { + SourceFile = sourceFile, Span = span, LeadingWhiteSpace = whiteSpace.Leading, TrailingWhiteSpace = whiteSpace.Trailing, }, ReportError = reportError, - SourceFile = sourceFile, DirectiveKind = name, DirectiveText = value, }; @@ -232,6 +233,11 @@ public static SourceFile Load(string filePath) return new SourceFile(filePath, SourceText.From(stream, encoding: null)); } + public SourceFile WithPath(string newPath) + { + return new SourceFile(newPath, Text); + } + public SourceFile WithText(SourceText newText) { return new SourceFile(Path, newText); @@ -283,6 +289,7 @@ internal abstract class CSharpDirective(in CSharpDirective.ParseInfo info) public readonly struct ParseInfo { + public required SourceFile SourceFile { get; init; } /// /// Span of the full line including the trailing line break. /// @@ -295,7 +302,6 @@ public readonly struct ParseContext { public required ParseInfo Info { get; init; } public required ErrorReporter ReportError { get; init; } - public required SourceFile SourceFile { get; init; } public required string DirectiveKind { get; init; } public required string DirectiveText { get; init; } } @@ -308,10 +314,11 @@ public readonly struct ParseContext case "property": return Property.Parse(context); case "package": return Package.Parse(context); case "project": return Project.Parse(context); + case "include" or "exclude": return IncludeOrExclude.Parse(context); default: - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.UnrecognizedDirective, context.DirectiveKind)); + context.ReportError(context.Info.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.UnrecognizedDirective, context.DirectiveKind)); return null; - }; + } } private static (string, string?)? ParseOptionalTwoParts(in ParseContext context, char separator) @@ -322,14 +329,14 @@ private static (string, string?)? ParseOptionalTwoParts(in ParseContext context, string directiveKind = context.DirectiveKind; if (firstPart.IsWhiteSpace()) { - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); + context.ReportError(context.Info.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); return null; } // If the name contains characters that resemble separators, report an error to avoid any confusion. if (Patterns.DisallowedNameCharacters.Match(context.DirectiveText, beginning: 0, length: firstPart.Length).Success) { - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.InvalidDirectiveName, directiveKind, separator)); + context.ReportError(context.Info.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.InvalidDirectiveName, directiveKind, separator)); return null; } @@ -405,7 +412,7 @@ public sealed class Property(in ParseInfo info) : Named(info) if (propertyValue is null) { - context.ReportError(context.SourceFile, context.Info.Span, FileBasedProgramsResources.PropertyDirectiveMissingParts); + context.ReportError(context.Info.SourceFile, context.Info.Span, FileBasedProgramsResources.PropertyDirectiveMissingParts); return null; } @@ -415,14 +422,14 @@ public sealed class Property(in ParseInfo info) : Named(info) } catch (XmlException ex) { - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.PropertyDirectiveInvalidName, ex.Message)); + context.ReportError(context.Info.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.PropertyDirectiveInvalidName, ex.Message), ex); return null; } if (propertyName.Equals("RestoreUseStaticGraphEvaluation", StringComparison.OrdinalIgnoreCase) && MSBuildUtilities.ConvertStringToBool(propertyValue)) { - context.ReportError(context.SourceFile, context.Info.Span, FileBasedProgramsResources.StaticGraphRestoreNotSupported); + context.ReportError(context.Info.SourceFile, context.Info.Span, FileBasedProgramsResources.StaticGraphRestoreNotSupported); } return new Property(context.Info) @@ -495,7 +502,7 @@ public Project(in ParseInfo info, string name) : base(info) if (directiveText.IsWhiteSpace()) { string directiveKind = context.DirectiveKind; - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); + context.ReportError(context.Info.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); return null; } @@ -533,14 +540,14 @@ public Project WithName(string name, NameKind kind) /// /// If the directive points to a directory, returns a new directive pointing to the corresponding project file. /// - public Project EnsureProjectFilePath(SourceFile sourceFile, ErrorReporter reportError) + public Project EnsureProjectFilePath(ErrorReporter reportError) { var resolvedName = Name; // If the path is a directory like '../lib', transform it to a project file path like '../lib/lib.csproj'. // Also normalize backslashes to forward slashes to ensure the directive works on all platforms. - var sourceDirectory = Path.GetDirectoryName(sourceFile.Path) - ?? throw new InvalidOperationException($"Source file path '{sourceFile.Path}' does not have a containing directory."); + var sourceDirectory = Path.GetDirectoryName(Info.SourceFile.Path) + ?? throw new InvalidOperationException($"Source file path '{Info.SourceFile.Path}' does not have a containing directory."); var resolvedProjectPath = Path.Combine(sourceDirectory, resolvedName.Replace('\\', '/')); if (Directory.Exists(resolvedProjectPath)) @@ -554,12 +561,12 @@ public Project EnsureProjectFilePath(SourceFile sourceFile, ErrorReporter report } else { - reportError(sourceFile, Info.Span, string.Format(FileBasedProgramsResources.InvalidProjectDirective, error)); + reportError(Info.SourceFile, Info.Span, string.Format(FileBasedProgramsResources.InvalidProjectDirective, error)); } } else if (!File.Exists(resolvedProjectPath)) { - reportError(sourceFile, Info.Span, + reportError(Info.SourceFile, Info.Span, string.Format(FileBasedProgramsResources.InvalidProjectDirective, string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, resolvedProjectPath))); } @@ -568,6 +575,138 @@ public Project EnsureProjectFilePath(SourceFile sourceFile, ErrorReporter report public override string ToString() => $"#:project {Name}"; } + + public enum IncludeOrExcludeKind + { + Include, + Exclude, + } + + /// + /// #:include or #:exclude directive. + /// + public sealed class IncludeOrExclude(in ParseInfo info) : Named(info) + { + private static readonly ImmutableArray<(string Extension, string ItemType)> s_knownExtensions = + [ + (".cs", "Compile"), + (".resx", "EmbeddedResource"), + (".json", "Content"), + (".razor", "Content"), + ]; + + internal static IEnumerable KnownItemTypes + => field ??= s_knownExtensions.Select(static t => t.ItemType).Distinct(); + + internal static string KnownExtensions + => field ??= string.Join(", ", s_knownExtensions.Select(static t => $"'{t.Extension}'")); + + /// + /// Preserved across calls, i.e., + /// this is the original directive text as entered by the user. + /// + public required string OriginalName { get; init; } + + public required IncludeOrExcludeKind Kind { get; init; } + + public string? ItemType { get; init; } + + public static new IncludeOrExclude? Parse(in ParseContext context) + { + var directiveText = context.DirectiveText; + if (directiveText.IsWhiteSpace()) + { + string directiveKind = context.DirectiveKind; + context.ReportError(context.Info.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); + return null; + } + + return new IncludeOrExclude(context.Info) + { + OriginalName = directiveText, + Name = directiveText, + Kind = KindFromString(context.DirectiveKind), + }; + } + + public IncludeOrExclude WithDeterminedItemType(ErrorReporter reportError) + { + Debug.Assert(ItemType is null); + + string? itemType = null; + foreach (var mapping in s_knownExtensions) + { + if (Name.EndsWith(mapping.Extension, StringComparison.OrdinalIgnoreCase)) + { + itemType = mapping.ItemType; + break; + } + } + + if (itemType is null) + { + reportError(Info.SourceFile, Info.Span, + string.Format(FileBasedProgramsResources.IncludeOrExcludeDirectiveUnknownFileType, $"#:{KindToString()}", KnownExtensions)); + return this; + } + + return new IncludeOrExclude(Info) + { + OriginalName = OriginalName, + Name = Name, + Kind = Kind, + ItemType = itemType, + }; + } + + public IncludeOrExclude WithName(string name) + { + if (Name == name) + { + return this; + } + + return new IncludeOrExclude(Info) + { + OriginalName = OriginalName, + Name = name, + Kind = Kind, + ItemType = ItemType, + }; + } + + private static IncludeOrExcludeKind KindFromString(string kind) + { + return kind switch + { + "include" => IncludeOrExcludeKind.Include, + "exclude" => IncludeOrExcludeKind.Exclude, + _ => throw new InvalidOperationException($"Unexpected include/exclude directive kind '{kind}'."), + }; + } + + public string KindToString() + { + return Kind switch + { + IncludeOrExcludeKind.Include => "include", + IncludeOrExcludeKind.Exclude => "exclude", + _ => throw new InvalidOperationException($"Unexpected {nameof(IncludeOrExcludeKind)} value '{Kind}'."), + }; + } + + public string KindToMSBuildString() + { + return Kind switch + { + IncludeOrExcludeKind.Include => "Include", + IncludeOrExcludeKind.Exclude => "Remove", + _ => throw new InvalidOperationException($"Unexpected {nameof(IncludeOrExcludeKind)} value '{Kind}'."), + }; + } + + public override string ToString() => $"#:{KindToString()} {Name}"; + } } /// @@ -618,18 +757,18 @@ public readonly struct Position } } -internal delegate void ErrorReporter(SourceFile sourceFile, TextSpan textSpan, string message); +internal delegate void ErrorReporter(SourceFile sourceFile, TextSpan textSpan, string message, Exception? innerException = null); internal static partial class ErrorReporters { public static readonly ErrorReporter IgnoringReporter = - static (_, _, _) => { }; + static (_, _, _, _) => { }; public static ErrorReporter CreateCollectingReporter(out ImmutableArray.Builder builder) { var capturedBuilder = builder = ImmutableArray.CreateBuilder(); - return (sourceFile, textSpan, message) => + return (sourceFile, textSpan, message, _) => capturedBuilder.Add(new SimpleDiagnostic { Location = new SimpleDiagnostic.Position() diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt b/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt index 9376a191aa0c..1675c0e7e02f 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt @@ -1,5 +1,20 @@ Microsoft.DotNet.FileBasedPrograms.CSharpDirective Microsoft.DotNet.FileBasedPrograms.CSharpDirective.CSharpDirective(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.IncludeOrExclude(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.ItemType.get -> string? +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.ItemType.init -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.Kind.get -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExcludeKind +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.Kind.init -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.KindToMSBuildString() -> string! +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.KindToString() -> string! +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.OriginalName.get -> string! +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.OriginalName.init -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.WithDeterminedItemType(Microsoft.DotNet.FileBasedPrograms.ErrorReporter! reportError) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude! +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.WithName(string! name) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude! +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExcludeKind +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExcludeKind.Exclude = 1 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExcludeKind +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExcludeKind.Include = 0 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExcludeKind Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Info.get -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Named Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Named.Name.get -> string! @@ -25,12 +40,14 @@ Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.LeadingWhiteSpace.get -> Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.LeadingWhiteSpace.init -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.ParseInfo() -> void +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.SourceFile.get -> Microsoft.DotNet.FileBasedPrograms.SourceFile +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.SourceFile.init -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.Span.get -> Microsoft.CodeAnalysis.Text.TextSpan Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.Span.init -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.TrailingWhiteSpace.get -> Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.TrailingWhiteSpace.init -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project -Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.EnsureProjectFilePath(Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, Microsoft.DotNet.FileBasedPrograms.ErrorReporter! reportError) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project! +Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.EnsureProjectFilePath(Microsoft.DotNet.FileBasedPrograms.ErrorReporter! reportError) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project! Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ExpandedName.get -> string? Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ExpandedName.init -> void Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind @@ -90,6 +107,7 @@ Microsoft.DotNet.FileBasedPrograms.SourceFile.SourceFile() -> void Microsoft.DotNet.FileBasedPrograms.SourceFile.SourceFile(string! Path, Microsoft.CodeAnalysis.Text.SourceText! Text) -> void Microsoft.DotNet.FileBasedPrograms.SourceFile.Text.get -> Microsoft.CodeAnalysis.Text.SourceText! Microsoft.DotNet.FileBasedPrograms.SourceFile.Text.init -> void +Microsoft.DotNet.FileBasedPrograms.SourceFile.WithPath(string! newPath) -> Microsoft.DotNet.FileBasedPrograms.SourceFile Microsoft.DotNet.FileBasedPrograms.SourceFile.WithText(Microsoft.CodeAnalysis.Text.SourceText! newText) -> Microsoft.DotNet.FileBasedPrograms.SourceFile Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.LineBreaks -> int @@ -97,12 +115,16 @@ Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.TotalLength -> int Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.WhiteSpaceInfo() -> void Microsoft.DotNet.ProjectTools.ProjectLocator override abstract Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ToString() -> string! +override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.ToString() -> string! override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.ToString() -> string! override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ToString() -> string! override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.ToString() -> string! override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk.ToString() -> string! override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Shebang.ToString() -> string! override Microsoft.DotNet.FileBasedPrograms.SourceFile.GetHashCode() -> int +static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.KnownExtensions.get -> string! +static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.KnownItemTypes.get -> System.Collections.Generic.IEnumerable! +static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.IncludeOrExclude? static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package? static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Named? static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project? @@ -126,6 +148,6 @@ static Microsoft.DotNet.FileBasedPrograms.SourceFile.operator ==(Microsoft.DotNe static Microsoft.DotNet.ProjectTools.ProjectLocator.TryGetProjectFileFromDirectory(string! projectDirectory, out string? projectFilePath, out string? error) -> bool static readonly Microsoft.DotNet.FileBasedPrograms.ErrorReporters.IgnoringReporter -> Microsoft.DotNet.FileBasedPrograms.ErrorReporter! static readonly Microsoft.DotNet.FileBasedPrograms.NamedDirectiveComparer.Instance -> Microsoft.DotNet.FileBasedPrograms.NamedDirectiveComparer! -virtual Microsoft.DotNet.FileBasedPrograms.ErrorReporter.Invoke(Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, Microsoft.CodeAnalysis.Text.TextSpan textSpan, string! message) -> void +virtual Microsoft.DotNet.FileBasedPrograms.ErrorReporter.Invoke(Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, Microsoft.CodeAnalysis.Text.TextSpan textSpan, string! message, System.Exception? innerException = null) -> void ~override Microsoft.DotNet.FileBasedPrograms.SourceFile.Equals(object obj) -> bool ~override Microsoft.DotNet.FileBasedPrograms.SourceFile.ToString() -> string \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf index e2ff54b63ea7..e35761618193 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf @@ -27,10 +27,10 @@ Duplicitní direktivy nejsou podporovány: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf index 35ea65672d5f..6d9f44c6f588 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf @@ -27,10 +27,10 @@ Doppelte Anweisungen werden nicht unterstützt: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf index 52838d81d438..f23f96cf5609 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf @@ -27,10 +27,10 @@ No se admiten directivas duplicadas: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf index c72828f206e1..91ee4e800682 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf @@ -27,10 +27,10 @@ Les directives dupliquées ne sont pas prises en charge : {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf index 1ef99c9b7878..caa07d902f78 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf @@ -27,10 +27,10 @@ Le direttive duplicate non supportate: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf index 9c3d5d6397c9..a5fb9b200897 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf @@ -27,10 +27,10 @@ 重複するディレクティブはサポートされていません: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf index 5fec6b68dc06..eae29cfdf78c 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf @@ -27,10 +27,10 @@ 중복 지시문은 지원되지 않습니다. {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf index 6a095b940a51..1a0b83d227c9 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf @@ -27,10 +27,10 @@ Zduplikowane dyrektywy nie są obsługiwane: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf index bc90e2b2187d..117b731327ad 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf @@ -27,10 +27,10 @@ Diretivas duplicadas não são suportadas:{0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf index 2cce6fdb2ccf..647890d77055 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf @@ -27,10 +27,10 @@ Повторяющиеся директивы не поддерживаются: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf index 83009813c12b..767756dcea1d 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf @@ -27,10 +27,10 @@ Yinelenen yönergeler desteklenmez: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf index 1a31fe1a755c..b67f7ed53df3 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf @@ -27,10 +27,10 @@ 不支持重复指令: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf index d2b441fee0d5..5b29d67fd583 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf @@ -27,10 +27,10 @@ 不支援重複的指示詞: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. diff --git a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs index 8ff1c997d93c..7c2bbed46ac4 100644 --- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs +++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs @@ -232,7 +232,11 @@ private int ExecuteForFileBasedApp(string path) { var lockFile = new LockFileFormat().Read(projectAssetsFile); var library = lockFile.Libraries.FirstOrDefault(l => string.Equals(l.Name, _packageId.Id, StringComparison.OrdinalIgnoreCase)); - if (library != null) + if (library == null) + { + Reporter.Verbose.WriteLine($"Package '{_packageId.Id}' not found in assets file: {projectAssetsFile}"); + } + else { var restoredVersion = library.Version.ToString(); if (central is { } centralValue) @@ -329,8 +333,7 @@ static void NoOp() { } { // Get the ItemGroup to add a PackageVersion to or create a new one. var itemGroup = directoryPackagesPropsProject.Xml.ItemGroups - .Where(e => e.Items.Any(i => string.Equals(i.ItemType, packageVersionItemType, StringComparison.OrdinalIgnoreCase))) - .FirstOrDefault() + .FirstOrDefault(e => e.Items.Any(i => string.Equals(i.ItemType, packageVersionItemType, StringComparison.OrdinalIgnoreCase))) ?? directoryPackagesPropsProject.Xml.AddItemGroup(); // Add a PackageVersion item. diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs index ee9a5e1d77a5..816b6dc2d1a0 100644 --- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs +++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs @@ -60,7 +60,7 @@ public override int Execute() } else { - VirtualProjectBuilder.RemoveDirectivesFromFile(evaluatedDirectives, builder.EntryPointSourceFile.Text, targetFile); + VirtualProjectBuilder.RemoveDirectivesFromFile(evaluatedDirectives, builder.EntryPointSourceFile, targetFile); } // Create project file. @@ -73,7 +73,10 @@ public override int Execute() { using var stream = File.Open(projectFile, FileMode.Create, FileAccess.Write); using var writer = new StreamWriter(stream, Encoding.UTF8); - VirtualProjectBuilder.WriteProjectFile(writer, UpdateDirectives(evaluatedDirectives), isVirtualProject: false, + VirtualProjectBuilder.WriteProjectFile( + writer, + UpdateDirectives(evaluatedDirectives), + isVirtualProject: false, userSecretsId: DetermineUserSecretsId(), defaultProperties: GetDefaultProperties()); } @@ -91,7 +94,24 @@ public override int Execute() string targetItemDirectory = Path.GetDirectoryName(targetItemFullPath)!; CreateDirectory(targetItemDirectory); - CopyFile(item.FullPath, targetItemFullPath); + + if (item.ItemType == "Compile") + { + if (dryRun) + { + Reporter.Output.WriteLine(CliCommandStrings.ProjectConvertWouldCopyFile, item.FullPath, targetItemFullPath); + Reporter.Output.WriteLine(CliCommandStrings.ProjectConvertWouldConvertFile, targetItemFullPath); + } + else + { + var sourceFile = SourceFile.Load(item.FullPath); + VirtualProjectBuilder.RemoveDirectivesFromFile(evaluatedDirectives, sourceFile, targetItemFullPath); + } + } + else + { + CopyFile(item.FullPath, targetItemFullPath); + } } return 0; @@ -123,14 +143,20 @@ void CopyFile(string source, string target) } } - IEnumerable<(string FullPath, string RelativePath)> FindIncludedItems() + IEnumerable<(string ItemType, string FullPath, string RelativePath)> FindIncludedItems() { string entryPointFileDirectory = PathUtility.EnsureTrailingSlash(Path.GetDirectoryName(file)!); // Include only items we know are files. string[] itemTypes = ["Content", "None", "Compile", "EmbeddedResource"]; + + Debug.Assert(CSharpDirective.IncludeOrExclude.KnownItemTypes.All(t => itemTypes.Contains(t)), + "We currently rely on conversion being able to copy files supported by include/exclude directives."); + var items = itemTypes.SelectMany(t => projectInstance.GetItems(t)); + var topLevelFileNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var item in items) { // Escape hatch - exclude items that have metadata `ExcludeFromFileBasedAppConversion` set to `true`. @@ -140,12 +166,7 @@ void CopyFile(string source, string target) continue; } - // Exclude items that are not contained within the entry point file directory. string itemFullPath = Path.GetFullPath(path: item.GetMetadataValue("FullPath"), basePath: entryPointFileDirectory); - if (!itemFullPath.StartsWith(entryPointFileDirectory, StringComparison.OrdinalIgnoreCase)) - { - continue; - } // Exclude items that do not exist. if (!File.Exists(itemFullPath)) @@ -154,7 +175,32 @@ void CopyFile(string source, string target) } string itemRelativePath = Path.GetRelativePath(relativeTo: entryPointFileDirectory, path: itemFullPath); - yield return (FullPath: itemFullPath, RelativePath: itemRelativePath); + + // Files outside the source directory should be copied into the target directory at the top level. + // Possibly with a number suffix to avoid conflicts. + // For C# files, this is needed so we can remove directives from them. + // For others, this is consistent but also we can omit the item groups from the converted project file and keep it simple. + if (itemRelativePath.StartsWith($"..{Path.DirectorySeparatorChar}", StringComparison.Ordinal)) + { + itemRelativePath = Path.GetFileName(itemFullPath); + string fileNameWithoutExtension; + string extension; + if (!topLevelFileNames.Add(itemRelativePath)) + { + fileNameWithoutExtension = Path.GetFileNameWithoutExtension(itemRelativePath); + extension = Path.GetExtension(itemRelativePath); + + var counter = 1; + do + { + counter++; + itemRelativePath = $"{fileNameWithoutExtension}_{counter}{extension}"; + } + while (!topLevelFileNames.Add(itemRelativePath)); + } + } + + yield return (item.ItemType, FullPath: itemFullPath, RelativePath: itemRelativePath); } } diff --git a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs index 8217e1744580..6c31fcd61e91 100644 --- a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs +++ b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs @@ -1,12 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Frozen; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.CommandLine; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Build.Evaluation; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.FileBasedPrograms; using Microsoft.DotNet.ProjectTools; @@ -65,18 +65,28 @@ public sealed class GetProject : RunApiInput public override RunApiOutput Execute() { - var sourceFile = SourceFile.Load(EntryPointFileFullPath); - var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: true, ErrorReporters.CreateCollectingReporter(out var diagnostics)); - string artifactsPath = ArtifactsPath ?? VirtualProjectBuilder.GetArtifactsPath(EntryPointFileFullPath); + var builder = new VirtualProjectBuilder( + entryPointFileFullPath: EntryPointFileFullPath, + targetFrameworkVersion: VirtualProjectBuildingCommand.TargetFrameworkVersion, + artifactsPath: ArtifactsPath); + + var errorReporter = ErrorReporters.CreateCollectingReporter(out var diagnostics); + + builder.CreateProjectInstance( + new ProjectCollection(), + errorReporter, + out _, + out var evaluatedDirectives, + validateAllDirectives: true); var csprojWriter = new StringWriter(); VirtualProjectBuilder.WriteProjectFile( csprojWriter, - directives, + evaluatedDirectives, VirtualProjectBuilder.GetDefaultProperties(VirtualProjectBuildingCommand.TargetFrameworkVersion), isVirtualProject: true, - targetFilePath: EntryPointFileFullPath, - artifactsPath: artifactsPath); + entryPointFilePath: EntryPointFileFullPath, + artifactsPath: builder.ArtifactsPath); return new RunApiOutput.Project { diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index 967cdc7fd884..519972139e98 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -221,6 +221,7 @@ public override int Execute() { if (result == 0) { + ReuseInfoFromPreviousCacheEntry(cache); MarkBuildSuccess(cache); } @@ -249,6 +250,13 @@ public override int Execute() Dictionary savedEnvironmentVariables = []; try { + // Set environment variables for MSBuild. + foreach (var (key, value) in MSBuildForwardingAppWithoutLogging.GetMSBuildRequiredEnvironmentVariables()) + { + savedEnvironmentVariables[key] = Environment.GetEnvironmentVariable(key); + Environment.SetEnvironmentVariable(key, value); + } + // Set environment variables. foreach (var (key, value) in MSBuildForwardingAppWithoutLogging.GetMSBuildRequiredEnvironmentVariables()) { @@ -326,6 +334,7 @@ public override int Execute() else { CacheCscArguments(cache, buildResult); + CollectAdditionalSources(cache, buildRequest.ProjectInstance); MarkBuildSuccess(cache); } @@ -361,15 +370,18 @@ public override int Execute() Environment.SetEnvironmentVariable(key, value); } - binaryLogger?.Value.ReallyShutdown(); + if (binaryLogger?.IsValueCreated == true) binaryLogger.Value.ReallyShutdown(); consoleLogger?.Shutdown(); } static Action> AddRestoreGlobalProperties(ReadOnlyDictionary? restoreProperties) { + // Compute the session ID outside the lambda to ensure it's the same for all project instances + // (since there can be multiple project instances created while evaluating file-level directives). + var sessionId = Guid.NewGuid().ToString("D"); return globalProperties => { - globalProperties["MSBuildRestoreSessionId"] = Guid.NewGuid().ToString("D"); + globalProperties["MSBuildRestoreSessionId"] = sessionId; globalProperties["MSBuildIsRestoring"] = bool.TrueString; foreach (var (key, value) in RestoringCommand.RestoreOptimizationProperties) { @@ -467,6 +479,48 @@ static string Escape(string arg) } } + void ReuseInfoFromPreviousCacheEntry(CacheInfo cache) + { + Debug.Assert(cache.CurrentEntry.AdditionalSources.Count == 0); + + if (cache.PreviousEntry != null) + { + foreach (var file in cache.PreviousEntry.AdditionalSources) + { + cache.CurrentEntry.AdditionalSources.Add(file); + } + } + } + + void CollectAdditionalSources(CacheInfo cache, ProjectInstance projectInstance) + { + // We intentionally ignore new additional sources in up-to-date check + // to avoid the overhead of MSBuild evaluation every time (even if the app is up to date). + // That can lead to missed changes but we are fine with that in rare cases + // (another example: we ignore changes to implicit build files imported transitively). + // Therefore, during up-to-date check, we only check the previously cached list of additional sources, + // and collect new ones only here after a re-build. + + Debug.Assert(cache.CurrentEntry.AdditionalSources.Count == 0); + + var entryPointFileDirectory = Path.GetDirectoryName(Builder.EntryPointFileFullPath); + Debug.Assert(entryPointFileDirectory != null); + + foreach (var itemType in CSharpDirective.IncludeOrExclude.KnownItemTypes) + { + foreach (var item in projectInstance.GetItems(itemType)) + { + var fullPath = Path.GetFullPath( + path: item.GetMetadataValue("FullPath"), + basePath: entryPointFileDirectory); + + cache.CurrentEntry.AdditionalSources.Add(fullPath); + } + } + + cache.CurrentEntry.AdditionalSources.Remove(Builder.EntryPointFileFullPath); + } + void PrintBuildInformation(ProjectCollection projectCollection, ProjectInstance projectInstance, BuildResult? buildOrRestoreResult) { var resultOutputFile = MSBuildArgs.GetResultOutputFile is [{ } file, ..] ? file : null; @@ -669,9 +723,11 @@ private CacheInfo ComputeCacheEntry() }; var entryPointFile = new FileInfo(Builder.EntryPointFileFullPath); + var entryPointFileDirectory = entryPointFile.Directory; + Debug.Assert(entryPointFileDirectory != null); // Collect current implicit build files. - CollectImplicitBuildFiles(entryPointFile.Directory, cacheEntry.ImplicitBuildFiles, out var exampleMSBuildFile); + CollectImplicitBuildFiles(entryPointFileDirectory, cacheEntry.ImplicitBuildFiles, out var exampleMSBuildFile); return new CacheInfo { @@ -682,9 +738,8 @@ private CacheInfo ComputeCacheEntry() } // internal for testing - internal static void CollectImplicitBuildFiles(DirectoryInfo? startDirectory, HashSet collectedPaths, out string? exampleMSBuildFile) + internal static void CollectImplicitBuildFiles(DirectoryInfo startDirectory, HashSet collectedPaths, out string? exampleMSBuildFile) { - Debug.Assert(startDirectory != null); exampleMSBuildFile = null; for (DirectoryInfo? directory = startDirectory; directory != null; directory = directory.Parent) { @@ -837,6 +892,20 @@ Building because previous global properties count ({previousCacheEntry.GlobalPro } } + // Check that additional sources are not modified. + // NOTE: We currently don't support the CSC-arg-reuse optimization through additional sources (i.e., we don't set `CanUseCscViaPreviousArguments=true` here). + // If that changes, we will also need to make sure `RunFileBuildCacheEntry.Directives` contains directives from other files + // (as that is used to determine whether we can reuse CSC args, see `GetReasonToNotReuseCscArguments`). + foreach (var additionalSourcePath in previousCacheEntry.AdditionalSources) + { + var additionalSourceFileInfo = ResolveLinkTargetOrSelf(new FileInfo(additionalSourcePath)); + if (!additionalSourceFileInfo.Exists || additionalSourceFileInfo.LastWriteTimeUtc > buildTimeUtc) + { + Reporter.Verbose.WriteLine("Building because additional source file is missing or modified: " + additionalSourceFileInfo.FullName); + return true; + } + } + // If we might be able to reuse CSC arguments, check whether the source file is modified. // NOTE: This must be the last check (otherwise setting cache.CanUseCscViaPreviousArguments would be incorrect). if (reasonToNotReuseCscArguments == null && targetFile.LastWriteTimeUtc > buildTimeUtc) @@ -1047,12 +1116,10 @@ public ProjectInstance CreateProjectInstance(ProjectCollection projectCollection projectCollection, ThrowingReporter, out var project, - out var evaluatedDirectives, + out _, Directives, addGlobalProperties); - Directives = evaluatedDirectives; - return project; } @@ -1076,7 +1143,9 @@ public static void CreateTempSubdirectory(string path) } public static readonly ErrorReporter ThrowingReporter = - static (sourceFile, textSpan, message) => throw new GracefulException($"{sourceFile.GetLocationString(textSpan)}: {FileBasedProgramsResources.DirectiveError}: {message}"); + static (sourceFile, textSpan, message, innerException) => throw new GracefulException( + $"{sourceFile.GetLocationString(textSpan)}: {FileBasedProgramsResources.DirectiveError}: {message}", + innerException); } internal sealed class RunFileBuildCacheEntry @@ -1099,10 +1168,17 @@ internal sealed class RunFileBuildCacheEntry public HashSet ImplicitBuildFiles { get; } /// - /// s recognized by the SDK (i.e., except shebang). + /// s from the entry point file recognized by the SDK (i.e., except shebang). /// public ImmutableArray Directives { get; set; } = []; + /// + /// Full paths of additional files that participate in the build + /// (e.g., default items like .resx and files from #:include directives). + /// + [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] + public HashSet AdditionalSources { get; } + public BuildLevel BuildLevel { get; set; } public string? SdkVersion { get; set; } // should be required and init-only but https://github.com/dotnet/runtime/issues/92877 @@ -1126,6 +1202,7 @@ public RunFileBuildCacheEntry() { GlobalProperties = new(GlobalPropertiesComparer); ImplicitBuildFiles = new(FilePathComparer); + AdditionalSources = new(FilePathComparer); } public RunFileBuildCacheEntry(Dictionary globalProperties) @@ -1133,6 +1210,7 @@ public RunFileBuildCacheEntry(Dictionary globalProperties) Debug.Assert(globalProperties.Comparer == GlobalPropertiesComparer); GlobalProperties = globalProperties; ImplicitBuildFiles = new(FilePathComparer); + AdditionalSources = new(FilePathComparer); } } diff --git a/src/Microsoft.DotNet.ProjectTools/Resources.resx b/src/Microsoft.DotNet.ProjectTools/Resources.resx index 0c69ccc49b2f..5c3d076e48a7 100644 --- a/src/Microsoft.DotNet.ProjectTools/Resources.resx +++ b/src/Microsoft.DotNet.ProjectTools/Resources.resx @@ -155,4 +155,11 @@ Make the profile names distinct. (Default) + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + diff --git a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs index ace65c8c4726..4830071cfd56 100644 --- a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs +++ b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs @@ -19,6 +19,8 @@ internal sealed class VirtualProjectBuilder { private readonly IEnumerable<(string name, string value)> _defaultProperties; + private (ImmutableArray Original, ImmutableArray Evaluated)? _evaluatedDirectives; + public string EntryPointFileFullPath { get; } public SourceFile EntryPointSourceFile @@ -43,7 +45,8 @@ public VirtualProjectBuilder( string entryPointFileFullPath, string targetFrameworkVersion, string[]? requestedTargets = null, - string? artifactsPath = null) + string? artifactsPath = null, + SourceText? sourceText = null) { Debug.Assert(Path.IsPathFullyQualified(entryPointFileFullPath)); @@ -51,6 +54,11 @@ public VirtualProjectBuilder( RequestedTargets = requestedTargets; ArtifactsPath = artifactsPath; _defaultProperties = GetDefaultProperties(targetFrameworkVersion); + + if (sourceText != null) + { + EntryPointSourceFile = new SourceFile(entryPointFileFullPath, sourceText); + } } /// @@ -88,7 +96,7 @@ public static string GetTempSubdirectory() if (string.IsNullOrEmpty(directory)) { - throw new InvalidOperationException(FileBasedProgramsResources.EmptyTempPath); + throw new InvalidOperationException(Resources.EmptyTempPath); } return Path.Join(directory, "dotnet", "runfile"); @@ -129,77 +137,161 @@ public static bool IsValidEntryPointPath(string entryPointFilePath) } /// - /// If there are any #:project , - /// evaluates their values as MSBuild expressions (i.e. substitutes $() and @() with property and item values, etc.) and - /// resolves the evaluated values to full project file paths (e.g. if the evaluted value is a directory finds a project in that directory). + /// Evaluates against a and the file system. /// - internal static ImmutableArray EvaluateDirectives( - ProjectInstance? project, + /// + /// All directives that need some other evaluation (described below) are expanded as MSBuild expressions + /// (i.e., $() and @() are substituted with property and item values, etc.). + /// + /// #:project directives are resolved to full project file paths + /// (e.g., if the evaluated value is a directory, finds a project in that directory). + /// + /// #:include/#:exclude have their determined + /// and relative paths resolved relative to their containing file. + /// + private static ImmutableArray EvaluateDirectives( + ProjectInstance project, ImmutableArray directives, - SourceFile sourceFile, - ErrorReporter errorReporter) + ErrorReporter reportError) { - if (directives.OfType().Any()) + if (!directives.Any(static d => d is CSharpDirective.Project or CSharpDirective.IncludeOrExclude)) { - return directives - .Select(d => d is CSharpDirective.Project p - ? (project is null - ? p - : p.WithName(project.ExpandString(p.Name), CSharpDirective.Project.NameKind.Expanded)) - .EnsureProjectFilePath(sourceFile, errorReporter) - : d) - .ToImmutableArray(); + return directives; } - return directives; + var builder = ImmutableArray.CreateBuilder(directives.Length); + + foreach (var directive in directives) + { + switch (directive) + { + case CSharpDirective.Project projectDirective: + projectDirective = projectDirective.WithName(project.ExpandString(projectDirective.Name), CSharpDirective.Project.NameKind.Expanded); + projectDirective = projectDirective.EnsureProjectFilePath(reportError); + + builder.Add(projectDirective); + break; + + case CSharpDirective.IncludeOrExclude includeOrExcludeDirective: + var expandedPath = project.ExpandString(includeOrExcludeDirective.Name); + var fullPath = Path.GetFullPath(path: expandedPath, basePath: Path.GetDirectoryName(includeOrExcludeDirective.Info.SourceFile.Path)!); + includeOrExcludeDirective = includeOrExcludeDirective.WithName(fullPath); + + // NOTE: In the future, instead of using only hard-coded item types here, + // we could read some user/sdk-defined MSBuild property from `project` for additional mapping. + includeOrExcludeDirective = includeOrExcludeDirective.WithDeterminedItemType(reportError); + + builder.Add(includeOrExcludeDirective); + break; + + default: + builder.Add(directive); + break; + } + } + + return builder.DrainToImmutable(); } public void CreateProjectInstance( ProjectCollection projectCollection, - ErrorReporter errorReporter, + ErrorReporter reportError, out ProjectInstance project, out ImmutableArray evaluatedDirectives, ImmutableArray directives = default, Action>? addGlobalProperties = null, bool validateAllDirectives = false) { + var directivesOriginal = directives; + if (directives.IsDefault) { - directives = FileLevelDirectiveHelpers.FindDirectives(EntryPointSourceFile, validateAllDirectives, errorReporter); + directives = FileLevelDirectiveHelpers.FindDirectives(EntryPointSourceFile, validateAllDirectives, reportError); } - project = CreateProjectInstance(projectCollection, directives, addGlobalProperties); + (string ProjectFileText, ProjectInstance ProjectInstance)? lastProject = null; - evaluatedDirectives = EvaluateDirectives(project, directives, EntryPointSourceFile, errorReporter); - if (evaluatedDirectives != directives) + // If we evaluated directives previously (e.g., during restore), reuse them. + // We don't use the additional properties from `addGlobalProperties` + // during directive evaluation anyway, so the directives can be reused safely. + if (_evaluatedDirectives is { } cached && + cached.Original == directivesOriginal) { - project = CreateProjectInstance(projectCollection, evaluatedDirectives, addGlobalProperties); + evaluatedDirectives = cached.Evaluated; + project = CreateProjectInstanceNoEvaluation( + projectCollection, + evaluatedDirectives, + addGlobalProperties); + return; } - } - private ProjectInstance CreateProjectInstance( - ProjectCollection projectCollection, - ImmutableArray directives, - Action>? addGlobalProperties = null) - { - var projectRoot = CreateProjectRootElement(projectCollection); + var entryPointDirectory = Path.GetDirectoryName(EntryPointFileFullPath)!; + var seenFiles = new HashSet(1, StringComparer.Ordinal) { EntryPointFileFullPath }; + var filesToProcess = new Queue(); + var evaluatedDirectiveBuilder = ImmutableArray.CreateBuilder(); - var globalProperties = projectCollection.GlobalProperties; - if (addGlobalProperties is not null) + do { - globalProperties = new Dictionary(projectCollection.GlobalProperties, StringComparer.OrdinalIgnoreCase); - addGlobalProperties(globalProperties); + // Create a project with properties from #:property directives so they can be expanded inside EvaluateDirectives. + project = CreateProjectInstanceNoEvaluation( + projectCollection, + [.. evaluatedDirectiveBuilder, .. directives], + addGlobalProperties); + + // Evaluate directives, e.g., determine item types for #:include/#:exclude from their file extension. + var fileEvaluatedDirectives = EvaluateDirectives(project, directives, reportError); + + evaluatedDirectiveBuilder.AddRange(fileEvaluatedDirectives); + + if (fileEvaluatedDirectives != directives) + { + // This project will contain items from #:include/#:exclude directives which we will traverse recursively. + project = CreateProjectInstanceNoEvaluation( + projectCollection, + evaluatedDirectiveBuilder.ToImmutable(), + addGlobalProperties); + } + + var compileItems = project.GetItems("Compile"); + foreach (var compileItem in compileItems) + { + var compilePath = Path.GetFullPath( + path: compileItem.GetMetadataValue("FullPath"), + basePath: entryPointDirectory); + if (seenFiles.Add(compilePath)) + { + filesToProcess.Enqueue(compilePath); + } + } } + while (TryGetNextFileToProcess()); + + evaluatedDirectives = evaluatedDirectiveBuilder.ToImmutable(); + _evaluatedDirectives = (directivesOriginal, evaluatedDirectives); - return ProjectInstance.FromProjectRootElement(projectRoot, new ProjectOptions + bool TryGetNextFileToProcess() { - ProjectCollection = projectCollection, - GlobalProperties = globalProperties, - }); + while (filesToProcess.TryDequeue(out var filePath)) + { + if (!File.Exists(filePath)) + { + reportError(EntryPointSourceFile, default, string.Format(Resources.IncludedFileNotFound, filePath)); + continue; + } + + var sourceFile = SourceFile.Load(filePath); + directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, validateAllDirectives, reportError); + return true; + } + + return false; + } - ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) + ProjectInstance CreateProjectInstanceNoEvaluation( + ProjectCollection projectCollection, + ImmutableArray directives, + Action>? addGlobalProperties = null) { - var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj"); var projectFileWriter = new StringWriter(); WriteProjectFile( @@ -207,17 +299,45 @@ ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) directives, _defaultProperties, isVirtualProject: true, - targetFilePath: EntryPointFileFullPath, + entryPointFilePath: EntryPointFileFullPath, artifactsPath: ArtifactsPath, includeRuntimeConfigInformation: RequestedTargets?.ContainsAny("Publish", "Pack") != true); var projectFileText = projectFileWriter.ToString(); - using var reader = new StringReader(projectFileText); - using var xmlReader = XmlReader.Create(reader); - var projectRoot = ProjectRootElement.Create(xmlReader, projectCollection); - projectRoot.FullPath = projectFileFullPath; - return projectRoot; + // If nothing changed, reuse the previous project instance to avoid unnecessary re-evaluations. + if (lastProject is { } cachedProject && cachedProject.ProjectFileText == projectFileText) + { + return cachedProject.ProjectInstance; + } + + var projectRoot = CreateProjectRootElement(projectFileText, projectCollection); + + var globalProperties = projectCollection.GlobalProperties; + if (addGlobalProperties is not null) + { + globalProperties = new Dictionary(projectCollection.GlobalProperties, StringComparer.OrdinalIgnoreCase); + addGlobalProperties(globalProperties); + } + + var result = ProjectInstance.FromProjectRootElement(projectRoot, new ProjectOptions + { + ProjectCollection = projectCollection, + GlobalProperties = globalProperties, + }); + + lastProject = (projectFileText, result); + + return result; + + ProjectRootElement CreateProjectRootElement(string projectFileText, ProjectCollection projectCollection) + { + using var reader = new StringReader(projectFileText); + using var xmlReader = XmlReader.Create(reader); + var projectRoot = ProjectRootElement.Create(xmlReader, projectCollection); + projectRoot.FullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj"); + return projectRoot; + } } } @@ -226,7 +346,7 @@ public static void WriteProjectFile( ImmutableArray directives, IEnumerable<(string name, string value)> defaultProperties, bool isVirtualProject, - string? targetFilePath = null, + string? entryPointFilePath = null, string? artifactsPath = null, bool includeRuntimeConfigInformation = true, string? userSecretsId = null) @@ -239,6 +359,7 @@ public static void WriteProjectFile( var propertyDirectives = directives.OfType(); var packageDirectives = directives.OfType(); var projectDirectives = directives.OfType(); + var includeOrExcludeDirectives = directives.OfType(); const string defaultSdkName = "Microsoft.NET.Sdk"; string firstSdkName; @@ -413,6 +534,43 @@ public static void WriteProjectFile( """); } + if (!isVirtualProject) + { + // In the real project, files are included by the conversion copying them to the output directory, + // hence we don't need to transfer the #:include/#:exclude directives over. + processedDirectives += includeOrExcludeDirectives.Count(); + } + else if (includeOrExcludeDirectives.Any()) + { + writer.WriteLine(""" + + """); + + foreach (var includeOrExclude in includeOrExcludeDirectives) + { + processedDirectives++; + + var itemType = includeOrExclude.ItemType; + + if (itemType == null) + { + // Before directives are evaluated, the item type is null. + // We still need to create the project (so that we can evaluate $() properties), + // but we can skip the items. + continue; + } + + writer.WriteLine($""" + <{itemType} {includeOrExclude.KindToMSBuildString()}="{EscapeValue(includeOrExclude.Name)}" /> + """); + } + + writer.WriteLine(""" + + + """); + } + if (packageDirectives.Any()) { writer.WriteLine(""" @@ -468,25 +626,25 @@ public static void WriteProjectFile( if (isVirtualProject) { - Debug.Assert(targetFilePath is not null); + Debug.Assert(entryPointFilePath is not null); - // Only add explicit Compile item when EnableDefaultCompileItems is not true. - // When EnableDefaultCompileItems=true, the file is included via default MSBuild globbing. - // See https://github.com/dotnet/sdk/issues/51785 + // We Exclude existing Compile items (which could be added e.g. + // in Microsoft.NET.Sdk.DefaultItems.props when user sets EnableDefaultCompileItems=true, + // or above via #:include/#:exclude directives). writer.WriteLine($""" - + """); if (includeRuntimeConfigInformation) { - var targetDirectory = Path.GetDirectoryName(targetFilePath) ?? ""; + var entryPointDirectory = Path.GetDirectoryName(entryPointFilePath) ?? ""; writer.WriteLine($""" - - + + """); @@ -531,29 +689,37 @@ static void WriteImport(TextWriter writer, string project, CSharpDirective.Sdk s } } - public static SourceText? RemoveDirectivesFromFile(ImmutableArray directives, SourceText text) + public static SourceFile RemoveDirectivesFromFile(ImmutableArray directives, SourceFile sourceFile) { if (directives.Length == 0) { - return null; + return sourceFile; } - Debug.Assert(directives.OrderBy(d => d.Info.Span.Start).SequenceEqual(directives), "Directives should be ordered by source location."); +#if DEBUG + var filteredDirectives = directives.Where(d => d.Info.SourceFile.Path == sourceFile.Path); + Debug.Assert( + filteredDirectives.OrderBy(static d => d.Info.Span.Start).SequenceEqual(filteredDirectives), + "Directives should be ordered by source location."); +#endif + + var text = sourceFile.Text; for (int i = directives.Length - 1; i >= 0; i--) { var directive = directives[i]; - text = text.Replace(directive.Info.Span, string.Empty); + if (directive.Info.SourceFile.Path == sourceFile.Path) + { + text = text.Replace(directive.Info.Span, string.Empty); + } } - return text; + return sourceFile.WithText(text); } - public static void RemoveDirectivesFromFile(ImmutableArray directives, SourceText text, string filePath) + public static void RemoveDirectivesFromFile(ImmutableArray directives, SourceFile sourceFile, string targetFilePath) { - if (RemoveDirectivesFromFile(directives, text) is { } modifiedText) - { - new SourceFile(filePath, modifiedText).Save(); - } + var modifiedFile = RemoveDirectivesFromFile(directives, sourceFile); + modifiedFile.WithPath(targetFilePath).Save(); } } diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.cs.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.cs.xlf index c336c2caa5d5..2109cf654f2e 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.cs.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.cs.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.de.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.de.xlf index 3538d5d20aec..c881be7e9f64 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.de.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.de.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.es.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.es.xlf index f409f1481066..0628a06ce586 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.es.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.es.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.fr.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.fr.xlf index af0c9d337ebf..74aae4a55a72 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.fr.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.fr.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.it.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.it.xlf index 129c9df27274..9c65f700dac9 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.it.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.it.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.ja.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.ja.xlf index 05e7fa1d39b8..84cd73720fa4 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.ja.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.ja.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.ko.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.ko.xlf index 7bc51a26dcd6..81336f2df5f6 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.ko.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.ko.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.pl.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.pl.xlf index 7e524550a5c8..1c99dd890a79 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.pl.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.pl.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.pt-BR.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.pt-BR.xlf index 2ff24ccb465b..f4387ab87411 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.pt-BR.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.pt-BR.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.ru.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.ru.xlf index 90e4e5879198..74c5f30a9c0e 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.ru.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.ru.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.tr.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.tr.xlf index 80e2d671ae91..a62c084a91af 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.tr.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.tr.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.zh-Hans.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.zh-Hans.xlf index 6fd81c1778a3..e25f1ae32a03 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.zh-Hans.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.zh-Hans.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.zh-Hant.xlf b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.zh-Hant.xlf index eadb9ad89a71..867307c17bb8 100644 --- a/src/Microsoft.DotNet.ProjectTools/xlf/Resources.zh-Hant.xlf +++ b/src/Microsoft.DotNet.ProjectTools/xlf/Resources.zh-Hant.xlf @@ -21,6 +21,16 @@ Make the profile names distinct. Make the profile names distinct. + + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + + + File included via #:include directive (or Compile item) not found: {0} + File included via #:include directive (or Compile item) not found: {0} + {Locked="#:include"}{Locked="Compile"}. {0} is file path. + A launch profile with the name '{0}' doesn't exist. A launch profile with the name '{0}' doesn't exist. diff --git a/test/Microsoft.NET.TestFramework/Assertions/DirectoryInfoAssertions.cs b/test/Microsoft.NET.TestFramework/Assertions/DirectoryInfoAssertions.cs index 0931e571dd49..e27e3d7cc86b 100644 --- a/test/Microsoft.NET.TestFramework/Assertions/DirectoryInfoAssertions.cs +++ b/test/Microsoft.NET.TestFramework/Assertions/DirectoryInfoAssertions.cs @@ -33,6 +33,20 @@ public AndConstraint HaveFile(string expectedFile) return new AndConstraint(this); } + public AndConstraint HaveFileContent(string relativePath, string expectedContent) + { + _dirInfo.File(relativePath).Should().Exist().And.Contain(expectedContent); + return new AndConstraint(this); + } + + public AndConstraint HaveFileContentPattern(string relativePath, string expectedContentWildcardPattern) + { + var file = _dirInfo.File(relativePath); + file.Should().Exist(); + File.ReadAllText(file.FullName).Should().Match(expectedContentWildcardPattern); + return new AndConstraint(this); + } + public AndConstraint NotHaveFile(string expectedFile) { var file = _dirInfo.EnumerateFiles(expectedFile, SearchOption.TopDirectoryOnly).SingleOrDefault() ?? new FileInfo(Path.Combine(_dirInfo.FullName, expectedFile)); @@ -151,5 +165,26 @@ public AndConstraint NotHaveSubDirectories(params strin return new AndConstraint(this); } + +#if NET + public AndConstraint HaveSubtree(string expectedSubtree) + { + string actualSubtree = string.Join(Environment.NewLine, _dirInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories) + .Select(f => + { + var path = Path.GetRelativePath(relativeTo: _dirInfo.FullName, path: f.FullName).Replace('\\', '/'); + return f is DirectoryInfo ? $"{path}/" : path; + }) + .Order(StringComparer.Ordinal)); + + actualSubtree.Should().Be(expected: expectedSubtree, because: $""" + actual is: + {actualSubtree} + + """); + + return new AndConstraint(this); + } +#endif } } diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs index 1b800a103c8b..b30849710063 100644 --- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs +++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs @@ -4,11 +4,11 @@ using System.Collections.Immutable; using System.Security; using System.Text.RegularExpressions; +using Microsoft.Build.Evaluation; using Microsoft.CodeAnalysis.Text; using Microsoft.DotNet.Cli.Commands; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Run.Tests; -using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.FileBasedPrograms; using Microsoft.DotNet.ProjectTools; @@ -595,14 +595,18 @@ public void DefaultItems_ExcludedViaMetadata() public void DefaultItems_ImplicitBuildFileInDirectory() { var testInstance = _testAssetsManager.CreateTestDirectory(); - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + + var srcDir = Path.Join(testInstance.Path, "src"); + Directory.CreateDirectory(srcDir); + + File.WriteAllText(Path.Join(srcDir, "Program.cs"), """ #:sdk Microsoft.NET.Sdk.Web Console.WriteLine(Util.GetText()); """); - File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), """ + File.WriteAllText(Path.Join(srcDir, "Util.cs"), """ class Util { public static string GetText() => "Hi from Util"; } """); - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + File.WriteAllText(Path.Join(srcDir, "Directory.Build.props"), """ @@ -613,29 +617,40 @@ class Util { public static string GetText() => "Hi from Util"; } // The app works before conversion. string expectedOutput = "Hi from Util"; new DotnetCommand(Log, "run", "Program.cs") - .WithWorkingDirectory(testInstance.Path) + .WithWorkingDirectory(srcDir) .Execute() .Should().Pass() .And.HaveStdOut(expectedOutput); // Convert. - new DotnetCommand(Log, "project", "convert", "Program.cs") - .WithWorkingDirectory(testInstance.Path) + new DotnetCommand(Log, "project", "convert", "Program.cs", "-o", "../out") + .WithWorkingDirectory(srcDir) .Execute() .Should().Pass(); - new DirectoryInfo(testInstance.Path) + new DirectoryInfo(srcDir) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Directory.Build.props", "Program", "Program.cs", "Util.cs"]); + .Should().BeEquivalentTo(["Directory.Build.props", "Program.cs", "Util.cs"]); + + var outDir = Path.Join(testInstance.Path, "out"); // Directory.Build.props is included as it's a None item. - new DirectoryInfo(Path.Join(testInstance.Path, "Program")) + new DirectoryInfo(outDir) .EnumerateFileSystemInfos().Select(f => f.Name).Order() .Should().BeEquivalentTo(["Directory.Build.props", "Program.csproj", "Program.cs", "Util.cs"]); - // The app works after conversion. - new DotnetCommand(Log, "run", "Program/Program.cs") - .WithWorkingDirectory(testInstance.Path) + // The app doesn't work immediately after conversion due to the Directory.Build.props file. + new DotnetCommand(Log, "run") + .WithWorkingDirectory(outDir) + .Execute() + .Should().Fail() + // error NETSDK1022: Duplicate 'Compile' items were included. + .And.HaveStdOutContaining("NETSDK1022"); + + File.Delete(Path.Join(outDir, "Directory.Build.props")); + + new DotnetCommand(Log, "run") + .WithWorkingDirectory(outDir) .Execute() .Should().Pass() .And.HaveStdOut(expectedOutput); @@ -645,7 +660,8 @@ class Util { public static string GetText() => "Hi from Util"; } public void DefaultItems_ImplicitBuildFileOutsideDirectory() { var testInstance = _testAssetsManager.CreateTestDirectory(); - var subdir = Path.Join(testInstance.Path, "subdir"); + var srcDir = Path.Join(testInstance.Path, "src"); + var subdir = Path.Join(srcDir, "subdir"); Directory.CreateDirectory(subdir); File.WriteAllText(Path.Join(subdir, "Program.cs"), """ Console.WriteLine(Util.GetText()); @@ -653,7 +669,7 @@ public void DefaultItems_ImplicitBuildFileOutsideDirectory() File.WriteAllText(Path.Join(subdir, "Util.cs"), """ class Util { public static string GetText() => "Hi from Util"; } """); - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + File.WriteAllText(Path.Join(srcDir, "Directory.Build.props"), """ @@ -670,22 +686,24 @@ class Util { public static string GetText() => "Hi from Util"; } .And.HaveStdOut(expectedOutput); // Convert. - new DotnetCommand(Log, "project", "convert", "Program.cs") + new DotnetCommand(Log, "project", "convert", "Program.cs", "-o", "../../out") .WithWorkingDirectory(subdir) .Execute() .Should().Pass(); new DirectoryInfo(subdir) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Program", "Program.cs", "Util.cs"]); + .Should().BeEquivalentTo(["Program.cs", "Util.cs"]); + + var outDir = Path.Join(testInstance.Path, "out"); - new DirectoryInfo(Path.Join(subdir, "Program")) + new DirectoryInfo(outDir) .EnumerateFileSystemInfos().Select(f => f.Name).Order() .Should().BeEquivalentTo(["Program.csproj", "Program.cs", "Util.cs"]); // The app works after conversion. - new DotnetCommand(Log, "run", "Program/Program.cs") - .WithWorkingDirectory(subdir) + new DotnetCommand(Log, "run") + .WithWorkingDirectory(outDir) .Execute() .Should().Pass() .And.HaveStdOut(expectedOutput); @@ -695,15 +713,16 @@ class Util { public static string GetText() => "Hi from Util"; } public void DefaultItems_ImplicitBuildFileAndUtilOutsideDirectory() { var testInstance = _testAssetsManager.CreateTestDirectory(); - var subdir = Path.Join(testInstance.Path, "subdir"); + var srcDir = Path.Join(testInstance.Path, "src"); + var subdir = Path.Join(srcDir, "subdir"); Directory.CreateDirectory(subdir); File.WriteAllText(Path.Join(subdir, "Program.cs"), """ Console.WriteLine(Util.GetText()); """); - File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), """ + File.WriteAllText(Path.Join(srcDir, "Util.cs"), """ class Util { public static string GetText() => "Hi from Util"; } """); - File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + File.WriteAllText(Path.Join(srcDir, "Directory.Build.props"), """ @@ -720,22 +739,24 @@ class Util { public static string GetText() => "Hi from Util"; } .And.HaveStdOut(expectedOutput); // Convert. - new DotnetCommand(Log, "project", "convert", "Program.cs") + new DotnetCommand(Log, "project", "convert", "Program.cs", "-o", "../../out") .WithWorkingDirectory(subdir) .Execute() .Should().Pass(); new DirectoryInfo(subdir) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Program", "Program.cs"]); + .Should().BeEquivalentTo(["Program.cs"]); - new DirectoryInfo(Path.Join(subdir, "Program")) + var outDir = Path.Join(testInstance.Path, "out"); + + new DirectoryInfo(outDir) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Program.csproj", "Program.cs"]); + .Should().BeEquivalentTo(["Program.csproj", "Program.cs", "Util.cs"]); // The app works after conversion. - new DotnetCommand(Log, "run", "Program/Program.cs") - .WithWorkingDirectory(subdir) + new DotnetCommand(Log, "run") + .WithWorkingDirectory(outDir) .Execute() .Should().Pass() .And.HaveStdOut(expectedOutput); @@ -1052,7 +1073,9 @@ public void ForceOption_On() [Fact] public void Directives() { + var testInstance = _testAssetsManager.CreateTestDirectory(); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #!/program #:sdk Microsoft.NET.Sdk @@ -1095,7 +1118,9 @@ public void Directives() [Fact] public void Directives_AllDefaultOverridden() { + var testInstance = _testAssetsManager.CreateTestDirectory(); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #!/program #:sdk Microsoft.NET.Web.Sdk @@ -1132,7 +1157,9 @@ public void Directives_AllDefaultOverridden() [Fact] public void Directives_Variable() { + var testInstance = _testAssetsManager.CreateTestDirectory(); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:package MyPackage@$(MyProp) #:property MyProp=MyValue @@ -1169,9 +1196,14 @@ public void Directives_DirectoryPath() Directory.CreateDirectory(libDir); File.WriteAllText(Path.Join(libDir, "Lib.csproj"), "test"); + var appDir = Path.Join(testInstance.Path, "app"); + Directory.CreateDirectory(appDir); + var slash = Path.DirectorySeparatorChar; VerifyConversion( - filePath: Path.Join(testInstance.Path, "app", "Program.cs"), + baseDirectory: testInstance.Path, + filePath: Path.Join(appDir, "Program.cs"), + evaluateDirectives: true, inputCSharp: """ #:project ../lib """, @@ -1197,10 +1229,84 @@ public void Directives_DirectoryPath() expectedCSharp: ""); } + [Fact] + public void Directives_IncludeExclude() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, + evaluateDirectives: true, + inputCSharp: """ + #:include A.cs + #:include ./**/*.cs + #:exclude B.cs + #:include C.ReSX + #:include D.json + #:include E.razor + #:include F.cshtml + #:exclude **/* + #:include |.cs + """, + expectedProject: $""" + + + + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true + + + + + """, + expectedCSharp: "", + expectedErrors: + [ + (7, string.Format(FileBasedProgramsResources.IncludeOrExcludeDirectiveUnknownFileType, "#:include", CSharpDirective.IncludeOrExclude.KnownExtensions)), + (8, string.Format(FileBasedProgramsResources.IncludeOrExcludeDirectiveUnknownFileType, "#:exclude", CSharpDirective.IncludeOrExclude.KnownExtensions)), + (1, string.Format(Resources.IncludedFileNotFound, Path.Join(testInstance.Path, "A.cs"))), + (1, string.Format(Resources.IncludedFileNotFound, Path.Join(testInstance.Path, "|.cs"))), + ]); + } + + [Fact] + public void Directives_IncludeExclude_FilesCopied() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + #:include **/*.cs + #:include *.json + #:exclude my.json + #:include */*.resx + Console.WriteLine(); + """); + File.WriteAllText(Path.Join(testInstance.Path, "my.json"), ""); + File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), ""); + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), ""); + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program", "Program.cs", "Resources.resx", "Util.cs", "my.json"]); + + new DirectoryInfo(Path.Join(testInstance.Path, "Program")) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program.csproj", "Program.cs", "Util.cs"]); + } + [Fact] public void Directives_Separators() { + var testInstance = _testAssetsManager.CreateTestDirectory(); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:property Prop1 = One=a/b #:property Prop2 = Two/a=b @@ -1245,35 +1351,57 @@ public void Directives_Separators() [InlineData("SDK")] public void Directives_Unknown(string directive) { - VerifyConversionThrows( + var testInstance = _testAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: $""" #:sdk Test #:{directive} Test """, - expectedWildcardPattern: RunFileTests.DirectiveError("/app/Program.cs", 2, FileBasedProgramsResources.UnrecognizedDirective, directive)); + expectedCSharp: $""" + #:{directive} Test + """, + expectedErrors: + [ + (2, string.Format(FileBasedProgramsResources.UnrecognizedDirective, directive)), + ]); } [Fact] public void Directives_Empty() { - VerifyConversionThrows( + var testInstance = _testAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #: #:sdk Test """, - expectedWildcardPattern: RunFileTests.DirectiveError("/app/Program.cs", 1, FileBasedProgramsResources.UnrecognizedDirective, "")); + expectedCSharp: """ + #: + + """, + expectedErrors: + [ + (1, string.Format(FileBasedProgramsResources.UnrecognizedDirective, "")), + ]); } [Theory, CombinatorialData] public void Directives_EmptyName( - [CombinatorialValues("sdk", "property", "package", "project")] string directive, + [CombinatorialValues("sdk", "property", "package", "project", "include", "exclude")] string directive, [CombinatorialValues(" ", "")] string value) { - VerifyConversionThrows( + var testInstance = _testAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: $""" #:{directive}{value} """, - expectedWildcardPattern: RunFileTests.DirectiveError("/app/Program.cs", 1, FileBasedProgramsResources.MissingDirectiveName, directive)); + expectedErrors: + [ + (1, string.Format(FileBasedProgramsResources.MissingDirectiveName, directive)), + ]); } [Theory] @@ -1281,7 +1409,9 @@ public void Directives_EmptyName( [InlineData(" ")] public void Directives_EmptyValue(string value) { + var testInstance = _testAssetsManager.CreateTestDirectory(); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: $""" #:property TargetFramework={value} #:property Prop1={value} @@ -1313,33 +1443,47 @@ public void Directives_EmptyValue(string value) """, expectedCSharp: ""); - VerifyConversionThrows( + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: $""" #:project{value} """, - expectedWildcardPattern: RunFileTests.DirectiveError("/app/Program.cs", 1, FileBasedProgramsResources.MissingDirectiveName, "project")); + expectedErrors: + [ + (1, string.Format(FileBasedProgramsResources.MissingDirectiveName, "project")), + ]); } [Fact] public void Directives_MissingPropertyValue() { - VerifyConversionThrows( + var testInstance = _testAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:property Test """, - expectedWildcardPattern: RunFileTests.DirectiveError("/app/Program.cs", 1, FileBasedProgramsResources.PropertyDirectiveMissingParts)); + expectedErrors: + [ + (1, FileBasedProgramsResources.PropertyDirectiveMissingParts), + ]); } [Fact] public void Directives_InvalidPropertyName() { - VerifyConversionThrows( + var testInstance = _testAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:property 123Name=Value """, - expectedWildcardPattern: RunFileTests.DirectiveError("/app/Program.cs", 1, FileBasedProgramsResources.PropertyDirectiveInvalidName, """ - Name cannot begin with the '1' character, hexadecimal value 0x31. - """)); + expectedErrors: + [ + (1, string.Format(FileBasedProgramsResources.PropertyDirectiveInvalidName, """ + Name cannot begin with the '1' character, hexadecimal value 0x31. + """)), + ]); } [Theory] @@ -1354,15 +1498,22 @@ public void Directives_InvalidPropertyName() [InlineData("property", "=", "@")] public void Directives_InvalidName(string directiveKind, string expectedSeparator, string actualSeparator) { - VerifyConversionThrows( + var testInstance = _testAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: $"#:{directiveKind} Abc{actualSeparator}Xyz", - expectedWildcardPattern: RunFileTests.DirectiveError("/app/Program.cs", 1, FileBasedProgramsResources.InvalidDirectiveName, directiveKind, expectedSeparator)); + expectedErrors: + [ + (1, string.Format(FileBasedProgramsResources.InvalidDirectiveName, directiveKind, expectedSeparator)), + ]); } [Fact] public void Directives_Escaping() { + var testInstance = _testAssetsManager.CreateTestDirectory(); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:property Prop= #:sdk @="<>te'st @@ -1410,7 +1561,9 @@ public void Directives_Escaping() [Fact] public void Directives_Whitespace() { + var testInstance = _testAssetsManager.CreateTestDirectory(); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #: sdk TestSdk #:property Name = Value @@ -1470,7 +1623,9 @@ public void Directives_BlankLines() """; + var testInstance = _testAssetsManager.CreateTestDirectory(); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:package A@B @@ -1483,6 +1638,7 @@ public void Directives_BlankLines() """); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:package A@B @@ -1509,13 +1665,10 @@ public void Directives_AfterToken() #:property Prop1=3 """; - VerifyConversionThrows( - inputCSharp: source, - expectedWildcardPattern: RunFileTests.DirectiveError("/app/Program.cs", 5, FileBasedProgramsResources.CannotConvertDirective)); - + var testInstance = _testAssetsManager.CreateTestDirectory(); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: source, - force: true, expectedProject: $""" @@ -1537,7 +1690,11 @@ public void Directives_AfterToken() #define X Console.WriteLine(); #:property Prop1=3 - """); + """, + expectedErrors: + [ + (5, FileBasedProgramsResources.CannotConvertDirective), + ]); } /// @@ -1556,13 +1713,10 @@ public void Directives_AfterIf() #:property Prop2=4 """; - VerifyConversionThrows( - inputCSharp: source, - expectedWildcardPattern: RunFileTests.DirectiveError("/app/Program.cs", 5, FileBasedProgramsResources.CannotConvertDirective)); - + var testInstance = _testAssetsManager.CreateTestDirectory(); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: source, - force: true, expectedProject: $""" @@ -1586,7 +1740,12 @@ public void Directives_AfterIf() #:property Prop1=3 #endif #:property Prop2=4 - """); + """, + expectedErrors: + [ + (5, FileBasedProgramsResources.CannotConvertDirective), + (7, FileBasedProgramsResources.CannotConvertDirective), + ]); } /// @@ -1595,7 +1754,9 @@ public void Directives_AfterIf() [Fact] public void Directives_Comments() { + var testInstance = _testAssetsManager.CreateTestDirectory(); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ // License for this file #:sdk MySdk @@ -1640,67 +1801,79 @@ public void Directives_Comments() [Fact] public void Directives_Duplicate() { - VerifyDirectiveConversionErrors( + var testInstance = _testAssetsManager.CreateTestDirectory(); + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:property Prop=1 #:property Prop=2 """, + expectedCSharp: "", expectedErrors: [ (2, string.Format(FileBasedProgramsResources.DuplicateDirective, "#:property Prop")), ]); - VerifyDirectiveConversionErrors( + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:sdk Name #:sdk Name@X #:sdk Name #:sdk Name2 """, + expectedCSharp: "", expectedErrors: [ (2, string.Format(FileBasedProgramsResources.DuplicateDirective, "#:sdk Name")), (3, string.Format(FileBasedProgramsResources.DuplicateDirective, "#:sdk Name")), ]); - VerifyDirectiveConversionErrors( + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:package Name #:package Name@X #:package Name #:package Name2 """, + expectedCSharp: "", expectedErrors: [ (2, string.Format(FileBasedProgramsResources.DuplicateDirective, "#:package Name")), (3, string.Format(FileBasedProgramsResources.DuplicateDirective, "#:package Name")), ]); - VerifyDirectiveConversionErrors( + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:sdk Prop@1 #:property Prop=2 """, - expectedErrors: []); + expectedCSharp: ""); - VerifyDirectiveConversionErrors( + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:property Prop=1 #:property Prop=2 #:property Prop2=3 #:property Prop=4 """, + expectedCSharp: "", expectedErrors: [ (2, string.Format(FileBasedProgramsResources.DuplicateDirective, "#:property Prop")), (4, string.Format(FileBasedProgramsResources.DuplicateDirective, "#:property Prop")), ]); - VerifyDirectiveConversionErrors( + VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:property prop=1 #:property PROP=2 """, + expectedCSharp: "", expectedErrors: [ (2, string.Format(FileBasedProgramsResources.DuplicateDirective, "#:property prop")), @@ -1710,7 +1883,9 @@ public void Directives_Duplicate() [Fact] // https://github.com/dotnet/sdk/issues/49797 public void Directives_VersionedSdkFirst() { + var testInstance = _testAssetsManager.CreateTestDirectory(); VerifyConversion( + baseDirectory: testInstance.Path, inputCSharp: """ #:sdk Microsoft.NET.Sdk@9.0.0 Console.WriteLine(); @@ -1735,62 +1910,96 @@ public void Directives_VersionedSdkFirst() """); } - private const string programPath = "/app/Program.cs"; + private static string GetProgramPath(string baseDirectory) + { + return Path.Join(baseDirectory, "Program.cs"); + } - private static void Convert(string inputCSharp, out string actualProject, out string? actualCSharp, bool force, string? filePath, - bool collectDiagnostics, out ImmutableArray.Builder? actualDiagnostics) + private static void Convert( + string inputCSharp, + out string actualProject, + out string? actualCSharp, + string filePath, + bool evaluateDirectives, + out ImmutableArray.Builder? actualDiagnostics) { - var sourceFile = new SourceFile(filePath ?? programPath, SourceText.From(inputCSharp, Encoding.UTF8)); - actualDiagnostics = null; - var diagnosticBag = collectDiagnostics ? ErrorReporters.CreateCollectingReporter(out actualDiagnostics) : VirtualProjectBuildingCommand.ThrowingReporter; - var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: !force, diagnosticBag); - directives = VirtualProjectBuilder.EvaluateDirectives(project: null, directives, sourceFile, diagnosticBag); + var builder = new VirtualProjectBuilder( + entryPointFileFullPath: filePath, + targetFrameworkVersion: VirtualProjectBuildingCommand.TargetFrameworkVersion, + sourceText: SourceText.From(inputCSharp, Encoding.UTF8)); + + var errorReporter = ErrorReporters.CreateCollectingReporter(out actualDiagnostics); + + ImmutableArray directives; + if (evaluateDirectives) + { + builder.CreateProjectInstance( + new ProjectCollection(), + errorReporter, + out _, + out directives); + } + else + { + directives = FileLevelDirectiveHelpers.FindDirectives( + builder.EntryPointSourceFile, + reportAllErrors: true, + errorReporter); + } + var projectWriter = new StringWriter(); - VirtualProjectBuilder.WriteProjectFile(projectWriter, directives, VirtualProjectBuilder.GetDefaultProperties(VirtualProjectBuildingCommand.TargetFrameworkVersion), isVirtualProject: false); + VirtualProjectBuilder.WriteProjectFile( + projectWriter, + directives, + VirtualProjectBuilder.GetDefaultProperties(VirtualProjectBuildingCommand.TargetFrameworkVersion), + isVirtualProject: false); + actualProject = projectWriter.ToString(); - actualCSharp = VirtualProjectBuilder.RemoveDirectivesFromFile(directives, sourceFile.Text)?.ToString(); + + var convertedFile = VirtualProjectBuilder.RemoveDirectivesFromFile(directives, builder.EntryPointSourceFile); + actualCSharp = convertedFile.Text != builder.EntryPointSourceFile.Text ? convertedFile.Text.ToString() : null; } + /// + /// means we don't care about the resulting project in this test. + /// /// /// means the conversion should not touch the C# content. /// - private static void VerifyConversion(string inputCSharp, string expectedProject, string? expectedCSharp, bool force = false, string? filePath = null, - IEnumerable<(int LineNumber, string Message)>? expectedErrors = null) + private static void VerifyConversion( + string baseDirectory, + string inputCSharp, + string? expectedProject = null, + string? expectedCSharp = null, + string? filePath = null, + IEnumerable<(int LineNumber, string Message)>? expectedErrors = null, + bool evaluateDirectives = false) { - Convert(inputCSharp, out var actualProject, out var actualCSharp, force: force, filePath: filePath, - collectDiagnostics: expectedErrors != null, out var actualDiagnostics); - actualProject.Should().Be(expectedProject); - actualCSharp.Should().Be(expectedCSharp); - VerifyErrors(actualDiagnostics, expectedErrors); - } + filePath ??= GetProgramPath(baseDirectory); - private static void VerifyConversionThrows(string inputCSharp, string expectedWildcardPattern) - { - var convert = () => Convert(inputCSharp, out _, out _, force: false, filePath: null, collectDiagnostics: false, out _); - convert.Should().Throw().WithMessage(expectedWildcardPattern); - } + Convert( + inputCSharp, + out var actualProject, + out var actualCSharp, + filePath: filePath, + evaluateDirectives: evaluateDirectives, + out var actualDiagnostics); - private static void VerifyDirectiveConversionErrors(string inputCSharp, IEnumerable<(int LineNumber, string Message)> expectedErrors) - { - var sourceFile = new SourceFile(programPath, SourceText.From(inputCSharp, Encoding.UTF8)); - FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: true, ErrorReporters.CreateCollectingReporter(out var diagnostics)); - VerifyErrors(diagnostics, expectedErrors); - } + if (expectedProject != null) actualProject.Should().Be(expectedProject); + actualCSharp.Should().Be(expectedCSharp); - private static void VerifyErrors(ImmutableArray.Builder? actual, IEnumerable<(int LineNumber, string Message)>? expected) - { - if (actual is null) + if (actualDiagnostics is null or []) { - Assert.Null(expected); + Assert.Null(expectedErrors); } - else if (expected is null) + else if (expectedErrors is null) { - Assert.Null(actual); + Assert.Null(actualDiagnostics); } else { - Assert.All(actual, d => { Assert.Equal(programPath, d.Location.Path); }); - actual.Select(d => (d.Location.Span.Start.Line + 1, d.Message)).Should().BeEquivalentTo(expected); + Assert.All(actualDiagnostics, d => { Assert.Equal(filePath, d.Location.Path); }); + actualDiagnostics.Select(d => (d.Location.Span.Start.Line + 1, d.Message)).Should().BeEquivalentTo(expectedErrors); } } } diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index e775adea5da0..ee66cdef1e4f 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -890,6 +890,44 @@ public void MultipleFiles_EnableDefaultCompileItemsViaDirectoryBuildProps() .And.HaveStdOut("Hello, String from Util"); } + /// + /// Directives in other files are considered even if those files are included via manual MSBuild rather than #:include. + /// + [Fact] + public void MultipleFiles_DirectivesInOtherFiles() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "A.cs"), """ + Console.WriteLine(B.M()); + #if !DEBUG + Console.WriteLine("Release config"); + #endif + """); + File.WriteAllText(Path.Join(testInstance.Path, "B.cs"), """ + #:property Configuration=Release + public static class B + { + public static string M() => "String from Util"; + } + """); + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + + + + """); + + new DotnetCommand(Log, "run", "A.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(""" + String from Util + Release config + """); + } + /// /// dotnet run util.cs fails if util.cs is not the entry-point. /// @@ -1425,7 +1463,7 @@ Release config } /// - /// dotnet run --bl file.cs produces a binary log. + /// dotnet run -bl file.cs produces a binary log. /// [Theory, CombinatorialData] public void BinaryLog_Run(bool beforeFile) @@ -1576,12 +1614,94 @@ public void BinaryLog_EvaluationData() string binaryLogPath = Path.Join(testInstance.Path, "msbuild.binlog"); new FileInfo(binaryLogPath).Should().Exist(); + // There should be exactly three - two for restore, one for build. + VerifyBinLogEvaluationDataCount(binaryLogPath, expectedCount: 3); + } + + private static void VerifyBinLogEvaluationDataCount(string binaryLogPath, int expectedCount) + { var records = BinaryLog.ReadRecords(binaryLogPath).ToList(); + records.Count(static r => r.Args is ProjectEvaluationStartedEventArgs).Should().Be(expectedCount); + records.Count(static r => r.Args is ProjectEvaluationFinishedEventArgs).Should().Be(expectedCount); + } + + /// + /// Binary logs from our in-memory projects should have evaluation data. + /// + [Fact] + public void BinaryLog_EvaluationData_MultiFile() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), + $""" + #:include *.cs + {s_programDependingOnUtil} + """); + + var utilPath = Path.Join(testInstance.Path, "Util.cs"); + File.WriteAllText(utilPath, s_util); + + new DotnetCommand(Log, "run", "--no-cache", "Program.cs", "-bl:first.binlog") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, String from Util"); + + string binaryLogPath = Path.Join(testInstance.Path, "first.binlog"); + new FileInfo(binaryLogPath).Should().Exist(); + + // There should be exactly four - two for restore and one for build as usual, plus one for initial directive evaluation. + var expectedCount = 4; + VerifyBinLogEvaluationDataCount(binaryLogPath, expectedCount: expectedCount); + + File.WriteAllText(utilPath, s_util.Replace("String from Util", "v2")); + + new DotnetCommand(Log, "run", "Program.cs", "-bl:second.binlog") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, v2"); + + binaryLogPath = Path.Join(testInstance.Path, "second.binlog"); + new FileInfo(binaryLogPath).Should().Exist(); + + // After rebuild, there should be the same number of evaluations. + VerifyBinLogEvaluationDataCount(binaryLogPath, expectedCount: expectedCount); + } + + /// + /// If we skip build due to up-to-date check, no binlog should be created. + /// + [Fact] + public void BinaryLog_EvaluationData_UpToDate() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, s_program); + + var expectedOutput = "Hello from Program"; + + new DotnetCommand(Log, "run", "--no-cache", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + + string binaryLogPath = Path.Join(testInstance.Path, "msbuild.binlog"); + new FileInfo(binaryLogPath).Should().NotExist(); + + new DotnetCommand(Log, "run", "Program.cs", "-bl") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut($""" + {CliCommandStrings.NoBinaryLogBecauseUpToDate} + {expectedOutput} + """); - // There should be at least two - one for restore, one for build. - // But the restore targets might re-evaluate the project via inner MSBuild task invocations. - records.Count(static r => r.Args is ProjectEvaluationStartedEventArgs).Should().BeGreaterThanOrEqualTo(2); - records.Count(static r => r.Args is ProjectEvaluationFinishedEventArgs).Should().BeGreaterThanOrEqualTo(2); + new FileInfo(binaryLogPath).Should().NotExist(); } [Theory, CombinatorialData] @@ -2659,7 +2779,7 @@ Hello from First Message: 'First1' """); - new DotnetCommand(Log, "run", "-v", "q", "Second.cs") + new DotnetCommand(Log, "run", "-v", "q", "Second.cs") .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Pass() @@ -2971,6 +3091,269 @@ public void ProjectReference_Duplicate(string? subdir) .And.HaveStdOut("Hello"); } + [Theory, CombinatorialData] + public void IncludeDirective( + [CombinatorialValues("Util.cs", "**/*.cs", "**/*.$(MyProp1)")] string includePattern, + [CombinatorialValues("", "#:exclude Program.$(MyProp1)")] string additionalDirectives) + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $""" + #:include {includePattern} + {additionalDirectives} + #:property MyProp1=cs + {s_programDependingOnUtil} + """); + File.WriteAllText(Path.Join(testInstance.Path, "Util.cs"), s_util); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello, String from Util"); + } + + [Fact] + public void IncludeDirective_WorkingDirectory() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var srcDir = Path.Join(testInstance.Path, "src"); + Directory.CreateDirectory(srcDir); + + var a = """ + Console.WriteLine(B.M()); + """; + + File.WriteAllText(Path.Join(srcDir, "A.cs"), $""" + #:include B.cs + {a} + """); + + var b = """ + static class B { public static string M() => "Hello from B"; } + """; + + File.WriteAllText(Path.Join(srcDir, "B.cs"), b); + + var expectedOutput = "Hello from B"; + + new DotnetCommand(Log, "run", "src/A.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + + // Convert to a project. + new DotnetCommand(Log, "project", "convert", "src/A.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(testInstance.Path) + .Should().HaveSubtree(""" + src/ + src/A.cs + src/A/ + src/A/A.cs + src/A/A.csproj + src/A/B.cs + src/B.cs + """) + .And.HaveFileContent("src/A/A.cs", a) + .And.HaveFileContent("src/A/B.cs", b) + .And.HaveFileContentPattern("src/A/A.csproj", """ + + + + Exe + net10.0 + enable + enable + true + true + A-* + + + + + """); + + // Run the converted project. + new DotnetCommand(Log, "run") + .WithWorkingDirectory(Path.Join(testInstance.Path, "src/A")) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + } + + [Fact] + public void IncludeDirective_Transitive() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + Directory.CreateDirectory(Path.Join(testInstance.Path, "dir1/dir2")); + Directory.CreateDirectory(Path.Join(testInstance.Path, "dir3")); + + var a = """ + B.M(); + """; + + File.WriteAllText(Path.Join(testInstance.Path, "dir1/A.cs"), $""" + #:include dir2/B.cs + {a} + """); + + var b = """ + static class B { public static void M() { C.M(); } } + """; + + File.WriteAllText(Path.Join(testInstance.Path, "dir1/dir2/B.cs"), $""" + #:include ../../dir3/$(P1).cs + #:property P1=C + {b} + """); + + var c = """ + static class C { public static void M() { D.M(); } } + """; + + File.WriteAllText(Path.Join(testInstance.Path, "dir3/C.cs"), $""" + #:include ../$(P1).cs + {c} + """); + + var d = """ + static class D + { + public static void M() + { + var asm = System.Reflection.Assembly.GetExecutingAssembly(); + using var stream = asm.GetManifestResourceStream($"{asm.GetName().Name}.Resources.resources")!; + using var reader = new System.Resources.ResourceReader(stream); + Console.WriteLine(reader.Cast().Single()); + } + } + """; + + File.WriteAllText(Path.Join(testInstance.Path, "C.cs"), $""" + #:include Resources.resx + {d} + """); + + File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx); + + var expectedOutput = "[MyString, TestValue]"; + + new DotnetCommand(Log, "run", "A.cs") + .WithWorkingDirectory(Path.Join(testInstance.Path, "dir1")) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + + // Convert to a project. + new DotnetCommand(Log, "project", "convert", "A.cs") + .WithWorkingDirectory(Path.Join(testInstance.Path, "dir1")) + .Execute() + .Should().Pass(); + + new DirectoryInfo(Path.Join(testInstance.Path, "dir1/A")) + .Should().HaveSubtree(""" + A.cs + A.csproj + C.cs + C_2.cs + Resources.resx + dir2/ + dir2/B.cs + """) + .And.HaveFileContent("A.cs", a) + .And.HaveFileContent("dir2/B.cs", b) + .And.HaveFileContent("C.cs", c) + .And.HaveFileContent("C_2.cs", d) + .And.HaveFileContent("Resources.resx", s_resx) + .And.HaveFileContentPattern("A.csproj", """ + + + + Exe + net10.0 + enable + enable + true + true + A-* + C + + + + + """); + + // Run the converted project. + new DotnetCommand(Log, "run") + .WithWorkingDirectory(Path.Join(testInstance.Path, "dir1/A")) + .Execute() + .Should().Pass() + .And.HaveStdOut(expectedOutput); + } + + [Fact] + public void IncludeDirective_FileNotFound() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "A.cs"); + + File.WriteAllText(programPath, """ + #:include B.cs + Console.WriteLine("Hello"); + """); + + new DotnetCommand(Log, "run", "A.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining(DirectiveError(programPath, 1, Resources.IncludedFileNotFound, Path.Join(testInstance.Path, "B.cs"))); + } + + /// + /// Combination of optimization and #:include directive. + /// + [Fact] + public void IncludeDirective_UpToDate() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programPath, $""" + #:include *.cs + {s_programDependingOnUtil} + """); + + var utilPath = Path.Join(testInstance.Path, "Util.cs"); + var utilCode = s_util; + File.WriteAllText(utilPath, utilCode); + + var artifactsDir = VirtualProjectBuilder.GetArtifactsPath(programPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + var expectedOutput = "Hello, String from Util"; + + Build(testInstance, BuildLevel.All, expectedOutput: expectedOutput); + + Build(testInstance, BuildLevel.None, expectedOutput: expectedOutput); + + utilCode = utilCode.Replace("String from Util", "v2"); + File.WriteAllText(utilPath, utilCode); + + Build(testInstance, BuildLevel.All, expectedOutput: "Hello, v2"); + + utilCode = utilCode.Replace("v2", "v3"); + File.WriteAllText(utilPath, utilCode); + + Build(testInstance, BuildLevel.All, expectedOutput: "Hello, v3"); + } + [Theory] // https://github.com/dotnet/aspnetcore/issues/63440 [InlineData(true, null)] [InlineData(false, null)] @@ -3718,9 +4101,12 @@ public class LibClass } /// - /// Up-to-date checks and optimizations currently don't support other included files. + /// optimization considers default items. + /// Also tests optimization. + /// (We cannot test because that optimization doesn't support neither #:property nor #:sdk which we need to enable default items.) + /// See . /// - [Theory, CombinatorialData] // https://github.com/dotnet/sdk/issues/50912 + [Theory, CombinatorialData] public void UpToDate_DefaultItems(bool optOut) { var testInstance = _testAssetsManager.CreateTestDirectory(); @@ -3730,60 +4116,67 @@ public void UpToDate_DefaultItems(bool optOut) {s_programReadingEmbeddedResource} """; File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), code); + + Build(testInstance, BuildLevel.All, expectedOutput: "Resource not found"); + File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx); - Build(testInstance, BuildLevel.All, expectedOutput: "[MyString, TestValue]"); + if (!optOut) + { + // Adding a default item is not recognized for perf reasons. + Build(testInstance, BuildLevel.None, expectedOutput: "Resource not found"); + Build(testInstance, BuildLevel.All, args: ["--no-cache"], expectedOutput: "[MyString, TestValue]"); + } + else + { + Build(testInstance, BuildLevel.All, expectedOutput: "[MyString, TestValue]"); + } // Update the RESX file. File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx.Replace("TestValue", "UpdatedValue")); - Build(testInstance, optOut ? BuildLevel.All : BuildLevel.None, expectedOutput: optOut ? "[MyString, UpdatedValue]" : "[MyString, TestValue]"); // note: outdated output (build skipped) + Build(testInstance, BuildLevel.All, expectedOutput: "[MyString, UpdatedValue]"); // Update the C# file. File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), "//v2\n" + code); - Build(testInstance, optOut ? BuildLevel.All : BuildLevel.Csc, expectedOutput: optOut ? "[MyString, UpdatedValue]" : "[MyString, TestValue]"); // note: outdated output (only CSC used) + Build(testInstance, optOut ? BuildLevel.All : BuildLevel.Csc, expectedOutput: "[MyString, UpdatedValue]"); - Build(testInstance, BuildLevel.All, ["--no-cache"], expectedOutput: "[MyString, UpdatedValue]"); + // Update the RESX file again (to verify the CSC only compilation didn't corrupt the list of additional files in the cache). + File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx.Replace("TestValue", "UpdatedValue2")); + + Build(testInstance, BuildLevel.All, expectedOutput: "[MyString, UpdatedValue2]"); } /// - /// Combination of with optimization. + /// Similar to but for .razor files instead of .resx files. /// - /// - /// Note: we cannot test because that optimization doesn't support neither #:property nor #:sdk which we need to enable default items. - /// - [Theory, CombinatorialData] // https://github.com/dotnet/sdk/issues/50912 - public void UpToDate_DefaultItems_CscOnly_AfterMSBuild(bool optOut) + [Fact] + public void UpToDate_DefaultItems_Razor() { - var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); - var code = $""" - #:property Configuration=Release - {(optOut ? "#:property FileBasedProgramCanSkipMSBuild=false" : "")} - #:property EnableDefaultEmbeddedResourceItems=true - {s_programReadingEmbeddedResource} - """; - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), code); - File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx); - - Build(testInstance, BuildLevel.All, expectedOutput: "[MyString, TestValue]"); - - // Update the C# file. - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), "//v2\n" + code); + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFileName = "MyRazorApp.cs"; + File.WriteAllText(Path.Join(testInstance.Path, programFileName), """ + #:sdk Microsoft.NET.Sdk.Web + _ = new MyRazorApp.MyCoolApp(); + Console.WriteLine("Hello from Program"); + """); - Build(testInstance, optOut ? BuildLevel.All : BuildLevel.Csc, expectedOutput: optOut ? "[MyString, TestValue]" : "[MyString, TestValue]"); + var razorFilePath = Path.Join(testInstance.Path, "MyCoolApp.razor"); + File.WriteAllText(razorFilePath, ""); - // Update the RESX file. - File.WriteAllText(Path.Join(testInstance.Path, "Resources.resx"), s_resx.Replace("TestValue", "UpdatedValue")); + Build(testInstance, BuildLevel.All, programFileName: programFileName); - Build(testInstance, optOut ? BuildLevel.All : BuildLevel.None, expectedOutput: optOut ? "[MyString, UpdatedValue]" : "[MyString, TestValue]"); + Build(testInstance, BuildLevel.None, programFileName: programFileName); - // Update the C# file. - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), "//v3\n" + code); + File.Delete(razorFilePath); - Build(testInstance, optOut ? BuildLevel.All : BuildLevel.Csc, expectedOutput: optOut ? "[MyString, UpdatedValue]" : "[MyString, TestValue]"); - - Build(testInstance, BuildLevel.All, ["--no-cache"], expectedOutput: "[MyString, UpdatedValue]"); + new DotnetCommand(Log, "run", programFileName) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + // error CS0246: The type or namespace name 'MyRazorApp' could not be found + .And.HaveStdOutContaining("error CS0246"); } [Fact] @@ -4232,7 +4625,7 @@ Release config code = code.Replace("Hello", "Hi"); File.WriteAllText(programPath, code); - Build(testInstance, BuildLevel.Csc, ["test", "args"], expectedOutput: """ + Build(testInstance, BuildLevel.Csc, args: ["test", "args"], expectedOutput: """ echo args:test;args Hi from Program Release config @@ -4611,7 +5004,7 @@ public void Api() File.WriteAllText(programPath, """ #!/program #:sdk Microsoft.NET.Sdk - #:sdk Aspire.Hosting.Sdk@9.1.0 + #:sdk Aspire.AppHost.Sdk@9.1.0 #:property TargetFramework=net11.0 #:package System.CommandLine@2.0.0-beta4.22272.1 #:property LangVersion=preview @@ -4649,7 +5042,7 @@ public void Api() - + net11.0 @@ -4663,7 +5056,87 @@ public void Api() - + + + + + + + + + + + + + + """)}},"Diagnostics":[]} + """); + } + + /// + /// Directives should be evaluated before the project for run-api is constructed. + /// + [Fact] + public void Api_Evaluation() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var programPath = Path.Join(testInstance.Path, "A.cs"); + File.WriteAllText(programPath, """ + #:property P1=cs + #:include B.$(P1) + Console.WriteLine(); + """); + + var bPath = Path.Join(testInstance.Path, "B.cs"); + File.WriteAllText(bPath, ""); + + new DotnetCommand(Log, "run-api") + .WithStandardInput($$""" + {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} + """) + .Execute() + .Should().Pass() + .And.HaveStdOut($$""" + {"$type":"Project","Version":1,"Content":{{ToJson($""" + + + + false + /artifacts + artifacts/$(MSBuildProjectName) + artifacts/$(MSBuildProjectName) + true + false + true + false + false + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true + + + + + + + + + + cs + false + $(Features);FileBasedProgram + + + + + + + + @@ -4672,7 +5145,6 @@ public void Api() - @@ -4730,7 +5202,7 @@ public void Api_Diagnostic_01() - + @@ -4800,7 +5272,7 @@ public void Api_Diagnostic_02() - +