From c7e186a8e6f950bc858360f72955a16288bec01f Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Thu, 25 Jun 2026 02:09:17 +0000 Subject: [PATCH] =?UTF-8?q?[msbuild]=20Source=20update=20a47a64a=20?= =?UTF-8?q?=E2=86=92=2033d1767=20Diff:=20https://github.com/dotnet/msbuild?= =?UTF-8?q?/compare/a47a64a9382828e968c3564d11f4e0afe99dc526..33d17672fac4?= =?UTF-8?q?c26656a201774f9894dff51a4e78?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From: https://github.com/dotnet/msbuild/commit/a47a64a9382828e968c3564d11f4e0afe99dc526 To: https://github.com/dotnet/msbuild/commit/33d17672fac4c26656a201774f9894dff51a4e78 [[ commit created by automation ]] --- .../documentation/release-checklist.md | 1 + .../Evaluation/Expander_Tests.cs | 126 +++++++ .../Microsoft.Build.Engine.UnitTests.csproj | 1 - ...r.ItemExpander.Transforms.MetadataMatch.cs | 37 +- ...emExpander.Transforms.MetadataMatchType.cs | 14 +- ...Transforms.OneOrMultipleMetadataMatches.cs | 22 +- .../Expander.ItemExpander.Transforms.cs | 99 +++--- .../Evaluation/Expander.MetadataExpander.cs | 332 +++++++++++------- .../Expander.MetadataMatchEvaluator.cs | 107 ------ .../Evaluation/Expander.RegularExpressions.cs | 224 ------------ .../Build/Evaluation/ExpressionShredder.cs | 171 +++++---- src/msbuild/src/Build/Microsoft.Build.csproj | 2 - .../src/Build/Utilities/ProjectWriter.cs | 2 + .../src/Framework.UnitTests/Assumed_Tests.cs | 44 +++ .../MSBuild.Benchmarks/ExpanderBenchmark.cs | 248 +++++++++++++ .../src/Package/MSBuild.VSSetup/files.swr | 2 +- .../Shared/UnitTests/ErrorUtilities_Tests.cs | 67 ---- .../Microsoft.Build.Tasks.UnitTests.csproj | 1 - ...Microsoft.Build.Utilities.UnitTests.csproj | 1 - src/source-manifest.json | 4 +- 20 files changed, 829 insertions(+), 676 deletions(-) delete mode 100644 src/msbuild/src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs delete mode 100644 src/msbuild/src/Build/Evaluation/Expander.RegularExpressions.cs create mode 100644 src/msbuild/src/MSBuild.Benchmarks/ExpanderBenchmark.cs delete mode 100644 src/msbuild/src/Shared/UnitTests/ErrorUtilities_Tests.cs diff --git a/src/msbuild/documentation/release-checklist.md b/src/msbuild/documentation/release-checklist.md index 3724a7754b4e..00d6af89a860 100644 --- a/src/msbuild/documentation/release-checklist.md +++ b/src/msbuild/documentation/release-checklist.md @@ -232,6 +232,7 @@ Steps are **mostly parallel** unless noted. - [ ] **5.4** Update `BootstrapSdkVersion` in [`eng/Versions.props`](https://github.com/dotnet/msbuild/blob/main/eng/Versions.props) if a fresh SDK was released. Check https://dotnet.microsoft.com/download/visual-studio-sdks — always verify the details for the targeted .NET version. - [ ] **5.4b** Update `tools.dotnet` in [`global.json`](https://github.com/dotnet/msbuild/blob/main/global.json) to the latest released SDK in the targeted band. - [ ] **5.5** Verify the overall subscription map across **every still-supported branch** — each `vsXX.Y` branch has an Arcade subscription matching its targeted .NET band, and each supported branch's outbound subscriptions land in the right downstream (e.g. SDK band, VMR). \ + You can find more info [here](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/52573/MSBuild-Maestro-Flow). - [ ] **5.6** Review this tracking issue for any process deviations. If the process changed, create a PR to update `documentation/release-checklist.md` with the improvements. --- diff --git a/src/msbuild/src/Build.UnitTests/Evaluation/Expander_Tests.cs b/src/msbuild/src/Build.UnitTests/Evaluation/Expander_Tests.cs index a3b47846bcd0..8349f64e1e95 100644 --- a/src/msbuild/src/Build.UnitTests/Evaluation/Expander_Tests.cs +++ b/src/msbuild/src/Build.UnitTests/Evaluation/Expander_Tests.cs @@ -1718,6 +1718,132 @@ public void ExpandAllIntoStringExpanderOptions() Assert.Equal(@"string$(p);dialogs%3b ; splash.bmp ; ; $(NonExistent) ; %(NonExistent) ; $(OutputPath) ; $(TargetPath) ; %(Language)_%(Culture)", expander.ExpandIntoStringAndUnescape(value, ExpanderOptions.ExpandItems, MockElementLocation.Instance)); } + /// + /// Builds an backed by a fixed metadata table for exercising the + /// hand-written metadata scanner. Metadata values intentionally contain no path separators so + /// that MaybeAdjustFilePath does not perturb the asserted results. + /// + private static Expander CreateMetadataExpander() + { + Dictionary metadata = new(StringComparer.OrdinalIgnoreCase) + { + ["Culture"] = "en-US", + ["Foo"] = "Bar", + ["Compile.Link"] = "Link.cs", + ["Filename"] = "App", + }; + + return new Expander( + new PropertyDictionary(), + new ItemDictionary(), + new StringMetadataTable(metadata), + FileSystems.Default); + } + + /// + /// Parity tests for the hand-written metadata scanner. These pin the exact expanded result for + /// whitespace handling, malformed references, nested references, qualified vs. unqualified + /// names, and missing metadata so future edits to the scanner cannot silently regress them. + /// + [Theory] + // Simple expansion, unqualified and qualified. + [InlineData("%(Culture)", "en-US")] + [InlineData("%(Foo)", "Bar")] + [InlineData("%(Compile.Link)", "Link.cs")] + // Whitespace around the parentheses and the dot separator is allowed. + [InlineData("%( Culture )", "en-US")] + [InlineData("%( Compile . Link )", "Link.cs")] + // Missing metadata expands to empty; a missing qualifier does not fall back to the unqualified key. + [InlineData("%(DoesNotExist)", "")] + [InlineData("%(Other.Foo)", "")] + // Malformed references are emitted verbatim. + [InlineData("%(", "%(")] + [InlineData("%()", "%()")] + [InlineData("%( )", "%( )")] + [InlineData("%(.x)", "%(.x)")] + // The outer reference is not closed by ')', so only the inner reference expands. + [InlineData("%(Culture%(Foo))", "%(CultureBar)")] + // Mixed with surrounding literal text and adjacent references. + [InlineData("prefix_%(Culture)_suffix", "prefix_en-US_suffix")] + [InlineData("%(Culture)%(Foo)", "en-USBar")] + public void ExpandMetadata_ScannerEdgeCases(string input, string expected) + { + Expander expander = CreateMetadataExpander(); + + expander.ExpandIntoStringLeaveEscaped(input, ExpanderOptions.ExpandMetadata, MockElementLocation.Instance) + .ShouldBe(expected); + } + + /// + /// Parity tests for metadata expansion in the gaps between (and within the separators of) item + /// vector expressions. Items are intentionally left unexpanded (ExpandMetadata only) so the + /// assertions isolate the gap/separator boundary handling in ScanAndExpandMetadataInGaps, + /// including the case where "@(" appears but does not form a well-formed item vector. + /// + [Theory] + // Metadata after, before, and between item vectors. + [InlineData("@(Compile)%(Culture)", "@(Compile)en-US")] + [InlineData("%(Culture)@(Compile)", "en-US@(Compile)")] + [InlineData("@(A)%(Culture)@(B)", "@(A)en-US@(B)")] + // A lone item vector has no gaps and is returned unchanged, even with embedded metadata in a transform. + [InlineData("@(Compile)", "@(Compile)")] + [InlineData("@(Compile->'%(Filename)')", "@(Compile->'%(Filename)')")] + // Metadata embedded in an item vector's separator is expanded in place. + [InlineData("@(Compile, '%(Culture)')", "@(Compile, 'en-US')")] + // "@(" that does not form a valid item vector still has its surrounding metadata expanded. + [InlineData("%(Culture)@(", "en-US@(")] + public void ExpandMetadata_ItemVectorGapsAndSeparators(string input, string expected) + { + Expander expander = CreateMetadataExpander(); + + expander.ExpandIntoStringLeaveEscaped(input, ExpanderOptions.ExpandMetadata, MockElementLocation.Instance) + .ShouldBe(expected); + } + + /// + /// Verifies the built-in vs. custom metadata gating in the scanner: a reference is expanded only + /// when the matching flag is set; otherwise it is emitted verbatim. + /// + /// + /// Declared internal because is internal; this assembly is + /// configured to discover non-public test methods. + /// + [Theory] + // Custom metadata (Culture) only expands with ExpandCustomMetadata. + [InlineData("%(Culture)", ExpanderOptions.ExpandCustomMetadata, "en-US")] + [InlineData("%(Culture)", ExpanderOptions.ExpandBuiltInMetadata, "%(Culture)")] + // Built-in metadata (Filename) only expands with ExpandBuiltInMetadata. + [InlineData("%(Filename)", ExpanderOptions.ExpandBuiltInMetadata, "App")] + [InlineData("%(Filename)", ExpanderOptions.ExpandCustomMetadata, "%(Filename)")] + internal void ExpandMetadata_BuiltInVsCustomGating(string input, ExpanderOptions options, string expected) + { + Expander expander = CreateMetadataExpander(); + + expander.ExpandIntoStringLeaveEscaped(input, options, MockElementLocation.Instance) + .ShouldBe(expected); + } + + /// + /// Parity test for the rewritten transform scanner (GetQuotedExpressionMatches): metadata + /// references inside a transform must not be qualified with an item name. This pins the error path + /// (and its message arguments) so the de-regexed scanner keeps rejecting qualified references, + /// including when surrounded by internal whitespace. + /// + [Theory] + [InlineData("@(i->'%(i.Meta0)')", "%(i.Meta0)")] + [InlineData("@(i->'%( i . Meta0 )')", "%( i . Meta0 )")] + public void Transform_QualifiedMetadataThrows(string input, string qualifiedReference) + { + Expander expander = CreateItemFunctionExpander(); + + InvalidProjectFileException exception = Should.Throw(() => + expander.ExpandIntoStringLeaveEscaped(input, ExpanderOptions.ExpandItems, MockElementLocation.Instance)); + + // The error reports the offending qualified reference and suggests the unqualified form. + exception.Message.ShouldContain(qualifiedReference); + exception.Message.ShouldContain("%(Meta0)"); + } + /// /// Exercises ExpandAllIntoStringListLeaveEscaped with a complex set of data. /// diff --git a/src/msbuild/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/msbuild/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj index 48b02992818d..5b0ce7d8c377 100644 --- a/src/msbuild/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj +++ b/src/msbuild/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -65,7 +65,6 @@ - diff --git a/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs b/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs index 8c1308404025..a9d4708d50c8 100644 --- a/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs +++ b/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatch.cs @@ -1,56 +1,43 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text.RegularExpressions; - #nullable disable namespace Microsoft.Build.Evaluation; internal partial class Expander - where P : class, IProperty - where I : class, IItem { private static partial class ItemExpander { private static partial class Transforms { /// - /// Represents a single match. Whether it was cached or from a Regex should be transparent - /// since we simulate the length calculation. + /// Represents a single match. Whether it was cached or from a manual scan should be transparent + /// since we simulate the length calculation. /// - private readonly struct MetadataMatch + private readonly struct MetadataMatch(int index, int length, string name) { public MetadataMatch(string name) + : this(index: 0, name.Length + QuotedExpressionSurroundCharCount, name) { - Name = name; - Index = 0; - Length = name.Length + QuotedExpressionSurroundCharCount; - } - - public MetadataMatch(Match match, string name) - { - Name = name; - Index = match.Index; - Length = match.Length; } /// - /// The inner value of the match. + /// Gets the inner value of the match. /// - internal string Name { get; } + internal string Name => name; /// - /// The index of the match in the original string. - /// If we have an exact string match, this will be 0. + /// Gets the index of the match in the original string. + /// If we have an exact string match, this will be 0. /// - internal int Index { get; } + internal int Index => index; /// - /// The length of the match in the original string. - /// If we have an exact string match, this computed to match the original input. + /// Gets the length of the match in the original string. + /// If we have an exact string match, this computed to match the original input. /// - internal int Length { get; } + internal int Length => length; } } } diff --git a/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs b/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs index 7ad7d22cb1ce..6200cef6839a 100644 --- a/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs +++ b/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.MetadataMatchType.cs @@ -4,36 +4,34 @@ namespace Microsoft.Build.Evaluation; internal partial class Expander - where P : class, IProperty - where I : class, IItem { private static partial class ItemExpander { private static partial class Transforms { /// - /// The type of match we found. - /// We use this to determine how to build the final output string. + /// The type of match we found. + /// We use this to determine how to build the final output string. /// private enum MetadataMatchType { /// - /// No matches found. The result will be empty. + /// No matches found. The result will be empty. /// None, /// - /// An exact full string match, e.g. '%(FullPath)'. + /// An exact full string match, e.g. '%(FullPath)'. /// ExactSingle, /// - /// A single match with surrounding characters, e.g. 'somedir/%(FileName)'. + /// A single match with surrounding characters, e.g. 'somedir/%(FileName)'. /// InexactSingle, /// - /// Multiple matches found, e.g. '%(FullPath)%(Extension)'. + /// Multiple matches found, e.g. '%(FullPath)%(Extension)'. /// Multiple, } diff --git a/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs b/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs index 191478a9124f..e6bffd7ef0a6 100644 --- a/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs +++ b/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.OneOrMultipleMetadataMatches.cs @@ -2,15 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Text.RegularExpressions; - -#nullable disable namespace Microsoft.Build.Evaluation; internal partial class Expander - where P : class, IProperty - where I : class, IItem { private static partial class ItemExpander { @@ -21,6 +16,12 @@ private static partial class Transforms /// private readonly struct OneOrMultipleMetadataMatches { + public static OneOrMultipleMetadataMatches None => default; + + public MetadataMatch Single { get; } + public List? Multiple { get; } + public MetadataMatchType Type { get; } + public OneOrMultipleMetadataMatches() { Type = MetadataMatchType.None; @@ -32,14 +33,15 @@ public OneOrMultipleMetadataMatches(string name) Single = new MetadataMatch(name); } - public OneOrMultipleMetadataMatches(string quotedExpressionFunction, Match match, string name) + public OneOrMultipleMetadataMatches(string quotedExpressionFunction, int matchIndex, int matchLength, string name) { // We know we have a full string match when our extracted name is the same length as the input // string minus the surrounding characters. Type = quotedExpressionFunction.Length == name.Length + QuotedExpressionSurroundCharCount ? MetadataMatchType.ExactSingle : MetadataMatchType.InexactSingle; - Single = new MetadataMatch(match, name); + + Single = new MetadataMatch(matchIndex, matchLength, name); } public OneOrMultipleMetadataMatches(List allMatches) @@ -47,12 +49,6 @@ public OneOrMultipleMetadataMatches(List allMatches) Type = MetadataMatchType.Multiple; Multiple = allMatches; } - - internal MetadataMatch Single { get; } - - internal List Multiple { get; } - - internal MetadataMatchType Type { get; } } } } diff --git a/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.cs b/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.cs index 90080660efa7..d218542291b9 100644 --- a/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.cs +++ b/src/msbuild/src/Build/Evaluation/Expander.ItemExpander.Transforms.cs @@ -10,7 +10,6 @@ #endif using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; @@ -28,8 +27,6 @@ namespace Microsoft.Build.Evaluation; internal partial class Expander - where P : class, IProperty - where I : class, IItem { private static partial class ItemExpander { @@ -605,9 +602,9 @@ internal static void ExpandQuotedExpressionFunction( } /// - /// Extracts a value from the input string based on a regular expression. - /// In the vast majority of cases, we'll only have 1-2 matches, and within those we can avoid allocating - /// the vast majority of Regex objects and return a cached result. + /// Scans the input string for unqualified metadata references of the form %(Name). + /// In the vast majority of cases, we'll only have 1-2 matches. + /// Qualified metadata (e.g. %(ItemType.Name)) is not allowed in transforms and will throw. /// private static OneOrMultipleMetadataMatches GetQuotedExpressionMatches(string quotedExpressionFunction, IElementLocation elementLocation) { @@ -618,60 +615,74 @@ private static OneOrMultipleMetadataMatches GetQuotedExpressionMatches(string qu return new OneOrMultipleMetadataMatches(cachedName); } - // GroupCollection + Groups are the most expensive source of allocations here, so we want to return - // before ever accessing the property. Simply accessing it will trigger the full collection - // allocation, so we avoid it unless absolutely necessary. - // Unfortunately even .NET Core does not have a struct-based Group enumerator at this point. - Match match = RegularExpressions.ItemMetadataRegex.Match(quotedExpressionFunction); - - if (!match.Success) + // Scan for %(Name) references manually. + int firstIndex = quotedExpressionFunction.IndexOf("%(", StringComparison.Ordinal); + if (firstIndex == -1) { - // No matches - the caller will use the original string. - return new OneOrMultipleMetadataMatches(); + return OneOrMultipleMetadataMatches.None; } - // From here will either return: - // 1. A single match, which may be offset within the input string.. - // 2. A list of multiple matches. List multipleMatches = null; - while (match.Success) + MetadataMatch? firstMatch = null; + + int pos = firstIndex; + while (pos < quotedExpressionFunction.Length - 1) { - // If true, this is likely an interpolated string, e.g. NETCOREAPP%(Identity)_OR_GREATER - bool isItemSpecModifier = s_itemSpecModifiers.TryGetValue(match.Value, out string name); - if (!isItemSpecModifier) + if (quotedExpressionFunction[pos] != '%' || quotedExpressionFunction[pos + 1] != '(') { - // Here is the worst case path which we've hopefully avoided at the point. - GroupCollection groupCollection = match.Groups; - name = groupCollection[RegularExpressions.NameGroup].Value; - ProjectErrorUtilities.VerifyThrowInvalidProject(groupCollection[RegularExpressions.ItemSpecificationGroup].Length == 0, elementLocation, "QualifiedMetadataInTransformNotAllowed", match.Value, name); + pos++; + continue; } - Match nextMatch = match.NextMatch(); + int refEnd = pos + 2; - // If we only have a single match, return before allocating the list. - bool isSingleMatch = multipleMatches == null && !nextMatch.Success; - if (isSingleMatch) + if (!ExpressionShredder.TryParseMetadataExpression(quotedExpressionFunction, ref refEnd, quotedExpressionFunction.Length, out string itemType, out string name)) { - OneOrMultipleMetadataMatches singleMatch = new(quotedExpressionFunction, match, name); + pos += 2; + continue; + } - // Only cache full string matches - skip known modifiers since they are permenantly cached. - if (singleMatch.Type == MetadataMatchType.ExactSingle && !isItemSpecModifier) - { - s_lastParsedQuotedExpression = name; - } + // Qualified metadata is not allowed in transforms. + if (itemType != null) + { + string matchValue = quotedExpressionFunction.Substring(pos, refEnd - pos); + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "QualifiedMetadataInTransformNotAllowed", matchValue, name); + } + + int matchLength = refEnd - pos; + + if (firstMatch == null) + { + firstMatch = new MetadataMatch(pos, matchLength, name); + } + else + { + multipleMatches ??= [firstMatch.Value]; + multipleMatches.Add(new MetadataMatch(pos, matchLength, name)); + } + + pos = refEnd; + } - return singleMatch; + if (multipleMatches != null) + { + return new OneOrMultipleMetadataMatches(multipleMatches); + } + + if (firstMatch != null) + { + MetadataMatch match = firstMatch.Value; + OneOrMultipleMetadataMatches singleMatch = new(quotedExpressionFunction, match.Index, match.Length, match.Name); + + if (singleMatch.Type == MetadataMatchType.ExactSingle && !ItemSpecModifiers.IsItemSpecModifier(match.Name)) + { + s_lastParsedQuotedExpression = match.Name; } - // We have multiple matches, so run the full loop. - // e.g. %(Filename)%(Extension) - // This is a very hot path, so we avoid allocating this until after we know there are multiple matches. - multipleMatches ??= []; - multipleMatches.Add(new MetadataMatch(match, name)); - match = nextMatch; + return singleMatch; } - return new OneOrMultipleMetadataMatches(multipleMatches); + return new OneOrMultipleMetadataMatches(); } /// diff --git a/src/msbuild/src/Build/Evaluation/Expander.MetadataExpander.cs b/src/msbuild/src/Build/Evaluation/Expander.MetadataExpander.cs index 8fe3c2f53b94..35a8ebd30a1e 100644 --- a/src/msbuild/src/Build/Evaluation/Expander.MetadataExpander.cs +++ b/src/msbuild/src/Build/Evaluation/Expander.MetadataExpander.cs @@ -2,179 +2,269 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Globalization; using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.NET.StringTools; -#nullable disable - namespace Microsoft.Build.Evaluation; internal partial class Expander - where P : class, IProperty - where I : class, IItem { /// - /// Expands bare metadata expressions, like %(Compile.WarningLevel), or unqualified, like %(Compile). + /// Expands bare metadata expressions, like %(Compile.WarningLevel), or unqualified, like %(Compile). /// /// - /// This is a private nested class, exposed only through the Expander class. - /// That allows it to hide its private methods even from Expander. + /// This is a private nested ref struct, exposed only through the static + /// entry point. /// - private static class MetadataExpander + private readonly ref struct MetadataExpander { + private readonly IMetadataTable _metadata; + private readonly ExpanderOptions _options; + private readonly IElementLocation _elementLocation; + private readonly LoggingContext? _loggingContext; + private readonly SpanBasedStringBuilder _builder; + + private MetadataExpander( + IMetadataTable metadata, + ExpanderOptions options, + IElementLocation elementLocation, + LoggingContext? loggingContext, + SpanBasedStringBuilder builder) + { + _metadata = metadata; + _options = options & (ExpanderOptions.ExpandMetadata | ExpanderOptions.Truncate | ExpanderOptions.LogOnItemMetadataSelfReference); + _elementLocation = elementLocation; + _loggingContext = loggingContext; + _builder = builder; + } + /// - /// Expands all embedded item metadata in the given string, using the bucketed items. - /// Metadata may be qualified, like %(Compile.WarningLevel), or unqualified, like %(Compile). + /// Expands all embedded item metadata in the given string, using the bucketed items. + /// Metadata may be qualified, like %(Compile.WarningLevel), or unqualified, like %(Compile). /// /// The expression containing item metadata references. /// The metadata to be expanded. /// Used to specify what to expand. /// The location information for error reporting purposes. /// The logging context for this operation. - /// The string with item metadata expanded in-place, escaped. - internal static string ExpandMetadataLeaveEscaped(string expression, IMetadataTable metadata, ExpanderOptions options, IElementLocation elementLocation, LoggingContext loggingContext = null) + /// + /// The string with item metadata expanded in-place, escaped. + /// + internal static string ExpandMetadataLeaveEscaped( + string expression, + IMetadataTable metadata, + ExpanderOptions options, + IElementLocation elementLocation, + LoggingContext? loggingContext = null) { - try + if ((options & ExpanderOptions.ExpandMetadata) == 0) { - if ((options & ExpanderOptions.ExpandMetadata) == 0) - { - return expression; - } + return expression; + } - Assumed.NotNull(metadata, "Cannot expand metadata without providing metadata"); + Assumed.NotNull(metadata, "Cannot expand metadata without providing metadata"); - // PERF NOTE: Regex matching is expensive, so if the string doesn't contain any item metadata references, just bail - // out -- pre-scanning the string is actually cheaper than running the Regex, even when there are no matches! - if (s_invariantCompareInfo.IndexOf(expression, "%(", CompareOptions.Ordinal) == -1) - { - return expression; - } + // PERF NOTE: pre-scanning the string for "%(" is cheaper than a full scan. + if (expression.IndexOf("%(", StringComparison.Ordinal) < 0) + { + return expression; + } - string result = null; + try + { + using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder(); + MetadataExpander expander = new(metadata, options, elementLocation, loggingContext, builder); - if (s_invariantCompareInfo.IndexOf(expression, "@(", CompareOptions.Ordinal) == -1) - { - // if there are no item vectors in the string - // run a simpler Regex to find item metadata references - MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext); + return expander.Expand(expression); + } + catch (InvalidOperationException ex) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotExpandItemMetadata", expression, ex.Message); + } - using SpanBasedStringBuilder finalResultBuilder = Strings.GetSpanBasedStringBuilder(); - RegularExpressions.ReplaceAndAppend(expression, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.ItemMetadataRegex); + return Assumed.Unreachable(); + } - // Don't create more strings - if (finalResultBuilder.Equals(expression.AsSpan())) - { - // If the final result is the same as the original expression, then just return the original expression - result = expression; - } - else - { - // Otherwise, convert the final result to a string - // and return that. - result = finalResultBuilder.ToString(); - } + private string Expand(string expression) + { + if (expression.IndexOf("@(", StringComparison.Ordinal) < 0) + { + // No item vectors in the string — scan for metadata references directly. + ScanAndExpandMetadata(expression); + } + else + { + ExpressionShredder.ReferencedItemExpressionsEnumerator enumerator = ExpressionShredder.GetReferencedItemExpressions(expression); + + if (!enumerator.MoveNext()) + { + // The string contains "@(" but no well-formed item vector expressions — + // scan the entire string for metadata references. + ScanAndExpandMetadata(expression); + } + else if (enumerator.Current.Value == expression + && enumerator.Current.Separator == null + && !enumerator.MoveNext()) + { + // The entire expression is a single item vector with no separator, so there are + // no gaps to expand metadata in — return the expression unchanged. + return expression; } else { - ExpressionShredder.ReferencedItemExpressionsEnumerator itemVectorExpressionsEnumerator = ExpressionShredder.GetReferencedItemExpressions(expression); + // Reuse the already-advanced enumerator (positioned at the first capture) to + // expand metadata in the gaps between item vector expressions. This avoids + // shredding the expression a second time. + ScanAndExpandMetadataInGaps(expression, ref enumerator); + } + } - // otherwise, run the more complex Regex to find item metadata references not contained in transforms - using SpanBasedStringBuilder finalResultBuilder = Strings.GetSpanBasedStringBuilder(); + return _builder.Equals(expression.AsSpan()) + ? expression + : _builder.ToString(); + } - int start = 0; + /// + /// Expands metadata in the gaps between item vector expressions and within their separators. + /// + /// + /// The supplied enumerator must already be positioned at the first capture (i.e. a successful + /// has been called). + /// Passing the already-advanced enumerator lets the caller's shred be reused instead of + /// re-scanning the expression from scratch. + /// + private void ScanAndExpandMetadataInGaps(string expression, ref ExpressionShredder.ReferencedItemExpressionsEnumerator enumerator) + { + int start = 0; - if (itemVectorExpressionsEnumerator.MoveNext()) - { - MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext); - ExpressionShredder.ItemExpressionCapture firstItemExpressionCapture = itemVectorExpressionsEnumerator.Current; - - if (itemVectorExpressionsEnumerator.MoveNext()) - { - // we're in the uncommon case with a partially enumerated enumerator. We need to process the first two items we enumerated and the remaining ones. - // Move over the expression, skipping those that have been recognized as an item vector expression - // Anything other than an item vector expression we want to expand bare metadata in. - start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, firstItemExpressionCapture); - start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, itemVectorExpressionsEnumerator.Current); - - while (itemVectorExpressionsEnumerator.MoveNext()) - { - start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, itemVectorExpressionsEnumerator.Current); - } - } - else - { - // There is only one item. Check to see if we're in the common case. - if (firstItemExpressionCapture.Value == expression && firstItemExpressionCapture.Separator == null) - { - // The most common case is where the transform is the whole expression - // Also if there were no valid item vector expressions found, then go ahead and do the replacement on - // the whole expression (which is what Orcas did). - return expression; - } - else - { - start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, firstItemExpressionCapture); - } - } - } + do + { + start = ProcessItemExpressionCapture(expression, start, enumerator.Current); + } + while (enumerator.MoveNext()); - // If there's anything left after the last item vector expression - // then we need to metadata replace and then append that - if (start < expression.Length) - { - MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext); - string subExpressionToReplaceIn = expression.Substring(start); + // Expand metadata in any trailing text after the last item vector expression. + if (start < expression.Length) + { + ScanAndExpandMetadata(expression, start, expression.Length); + } + } - RegularExpressions.ReplaceAndAppend(subExpressionToReplaceIn, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex); - } + private int ProcessItemExpressionCapture(string expression, int start, ExpressionShredder.ItemExpressionCapture itemExpressionCapture) + { + // Expand metadata in the gap before this item vector expression. + if (itemExpressionCapture.Index > start) + { + ScanAndExpandMetadata(expression, start, itemExpressionCapture.Index); + } - if (finalResultBuilder.Equals(expression.AsSpan())) - { - // If the final result is the same as the original expression, then just return the original expression - result = expression; - } - else - { - // Otherwise, convert the final result to a string - // and return that. - result = finalResultBuilder.ToString(); - } - } + // Expand metadata that appears in the item vector expression's separator. + if (itemExpressionCapture.Separator != null) + { + // Append the portion before the separator verbatim, then expand within the separator portion. + string value = itemExpressionCapture.Value; + int separatorStart = itemExpressionCapture.SeparatorStart; - return result; + _builder.Append(value, 0, separatorStart); + ScanAndExpandMetadata(value, separatorStart, value.Length); } - catch (InvalidOperationException ex) + else { - ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotExpandItemMetadata", expression, ex.Message); + // Append the item vector expression as-is. + _builder.Append(itemExpressionCapture.Value); } - return null; + // Advance past this item vector expression. + return itemExpressionCapture.Index + itemExpressionCapture.Length; + } - static int ProcessItemExpressionCapture(string expression, SpanBasedStringBuilder finalResultBuilder, MetadataMatchEvaluator matchEvaluator, int start, ExpressionShredder.ItemExpressionCapture itemExpressionCapture) + /// + private void ScanAndExpandMetadata(string input) + => ScanAndExpandMetadata(input, 0, input.Length); + + /// + /// Scans the specified range of for item metadata references + /// of the form %(Name) or %(ItemType.Name), expands them using the + /// provided metadata table, and appends the results to the builder. + /// + /// + /// A valid metadata name starts with a letter or underscore, followed by zero or more + /// letters, digits, underscores, or hyphens: [A-Za-z_][A-Za-z_0-9\-]*. This grammar is + /// enforced by and must stay in sync + /// with . + /// Whitespace is allowed around the parentheses and the dot separator. + /// If a %( sequence does not form a valid metadata reference, it is appended + /// to the output verbatim. + /// + private void ScanAndExpandMetadata(string input, int startIndex, int endIndex) + { + int lastCopied = startIndex; + + int i = input.IndexOf("%(", startIndex, StringComparison.Ordinal); + + while (i >= 0 && i < endIndex - 1) { - // Extract the part of the expression that appears before the item vector expression - // e.g. the ABC in ABC@(foo->'%(FullPath)') - string subExpressionToReplaceIn = expression.Substring(start, itemExpressionCapture.Index - start); + int pos = i + 2; - RegularExpressions.ReplaceAndAppend(subExpressionToReplaceIn, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex); + if (!ExpressionShredder.TryParseMetadataExpression(input, ref pos, endIndex, out string itemType, out string metadataName)) + { + // Not a valid metadata reference — skip past '%(' and keep scanning. + i = input.IndexOf("%(", i + 2, StringComparison.Ordinal); + continue; + } - // Expand any metadata that appears in the item vector expression's separator - if (itemExpressionCapture.Separator != null) + // Append everything before this reference. + if (i > lastCopied) { - RegularExpressions.ReplaceAndAppend(itemExpressionCapture.Value, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, -1, itemExpressionCapture.SeparatorStart, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex); + _builder.Append(input, lastCopied, i - lastCopied); + } + + // Determine whether to expand this metadata reference. + bool isBuiltInMetadata = ItemSpecModifiers.IsItemSpecModifier(metadataName); + + if ((isBuiltInMetadata && ((_options & ExpanderOptions.ExpandBuiltInMetadata) != 0)) || + (!isBuiltInMetadata && ((_options & ExpanderOptions.ExpandCustomMetadata) != 0))) + { + string expanded = _metadata.GetEscapedValue(itemType, metadataName); + + if ((_options & ExpanderOptions.LogOnItemMetadataSelfReference) != 0 && + _loggingContext != null && + !string.IsNullOrEmpty(metadataName) && + _metadata is IItemTypeDefinition itemMetadata && + (string.IsNullOrEmpty(itemType) || string.Equals(itemType, itemMetadata.ItemType, StringComparison.Ordinal))) + { + _loggingContext.LogComment( + MessageImportance.Low, + new BuildEventFileInfo(_elementLocation), + "ItemReferencingSelfInTarget", + itemMetadata.ItemType, + metadataName); + } + + if (IsTruncationEnabled(_options) && expanded.Length > CharacterLimitPerExpansion) + { + expanded = TruncateString(expanded); + } + + _builder.Append(expanded); } else { - // Append the item vector expression as is - // e.g. the @(foo->'%(FullPath)') in ABC@(foo->'%(FullPath)') - finalResultBuilder.Append(itemExpressionCapture.Value); + _builder.Append(input, i, pos - i); } - // Move onto the next part of the expression that isn't an item vector expression - start = (itemExpressionCapture.Index + itemExpressionCapture.Length); - return start; + lastCopied = pos; + + // Continue scanning after this reference. + i = input.IndexOf("%(", pos, StringComparison.Ordinal); + } + + // Append any remaining text after the last reference. + if (lastCopied < endIndex) + { + _builder.Append(input, lastCopied, endIndex - lastCopied); } } } diff --git a/src/msbuild/src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs b/src/msbuild/src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs deleted file mode 100644 index c927893ba8b7..000000000000 --- a/src/msbuild/src/Build/Evaluation/Expander.MetadataMatchEvaluator.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Text.RegularExpressions; -using Microsoft.Build.BackEnd.Logging; -using Microsoft.Build.Framework; -using Microsoft.Build.Shared; -using ItemSpecModifiers = Microsoft.Build.Framework.ItemSpecModifiers; - -#nullable disable - -namespace Microsoft.Build.Evaluation; - -internal partial class Expander - where P : class, IProperty - where I : class, IItem -{ - /// - /// A functor that returns the value of the metadata in the match - /// that is contained in the metadata dictionary it was created with. - /// - private struct MetadataMatchEvaluator - { - /// - /// Source of the metadata. - /// - private IMetadataTable _metadata; - - /// - /// Whether to expand built-in metadata, custom metadata, or both kinds. - /// - private ExpanderOptions _options; - - private IElementLocation _elementLocation; - - private LoggingContext _loggingContext; - - /// - /// Constructor taking a source of metadata. - /// - internal MetadataMatchEvaluator( - IMetadataTable metadata, - ExpanderOptions options, - IElementLocation elementLocation, - LoggingContext loggingContext) - { - _metadata = metadata; - _options = options & (ExpanderOptions.ExpandMetadata | ExpanderOptions.Truncate | ExpanderOptions.LogOnItemMetadataSelfReference); - _elementLocation = elementLocation; - _loggingContext = loggingContext; - - Assumed.NotEqual(options, ExpanderOptions.Invalid, "Must be expanding metadata of some kind"); - } - - /// - /// Expands a single item metadata, which may be qualified with an item type. - /// - internal static string ExpandSingleMetadata(Match itemMetadataMatch, MetadataMatchEvaluator evaluator) - { - Assumed.True(itemMetadataMatch.Success, "Need a valid item metadata."); - - string metadataName = itemMetadataMatch.Groups[RegularExpressions.NameGroup].Value; - - string metadataValue = null; - - bool isBuiltInMetadata = ItemSpecModifiers.IsItemSpecModifier(metadataName); - - if ( - (isBuiltInMetadata && ((evaluator._options & ExpanderOptions.ExpandBuiltInMetadata) != 0)) || - (!isBuiltInMetadata && ((evaluator._options & ExpanderOptions.ExpandCustomMetadata) != 0))) - { - string itemType = null; - - // check if the metadata is qualified with the item type - if (itemMetadataMatch.Groups[RegularExpressions.ItemSpecificationGroup].Length > 0) - { - itemType = itemMetadataMatch.Groups[RegularExpressions.ItemTypeGroup].Value; - } - - metadataValue = evaluator._metadata.GetEscapedValue(itemType, metadataName); - - if ((evaluator._options & ExpanderOptions.LogOnItemMetadataSelfReference) != 0 && - evaluator._loggingContext != null && - !string.IsNullOrEmpty(metadataName) && - evaluator._metadata is IItemTypeDefinition itemMetadata && - (string.IsNullOrEmpty(itemType) || string.Equals(itemType, itemMetadata.ItemType, StringComparison.Ordinal))) - { - evaluator._loggingContext.LogComment(MessageImportance.Low, new BuildEventFileInfo(evaluator._elementLocation), - "ItemReferencingSelfInTarget", itemMetadata.ItemType, metadataName); - } - - if (IsTruncationEnabled(evaluator._options) && metadataValue.Length > CharacterLimitPerExpansion) - { - metadataValue = TruncateString(metadataValue); - } - } - else - { - // look up the metadata - we may not have a value for it - metadataValue = itemMetadataMatch.Value; - } - - return metadataValue; - } - } -} diff --git a/src/msbuild/src/Build/Evaluation/Expander.RegularExpressions.cs b/src/msbuild/src/Build/Evaluation/Expander.RegularExpressions.cs deleted file mode 100644 index 5660542779fd..000000000000 --- a/src/msbuild/src/Build/Evaluation/Expander.RegularExpressions.cs +++ /dev/null @@ -1,224 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using Microsoft.Build.Shared; -using Microsoft.NET.StringTools; - -#nullable disable - -namespace Microsoft.Build.Evaluation; - -internal partial class Expander - where P : class, IProperty - where I : class, IItem -{ - /// - /// Regular expressions used by the expander. - /// The expander currently uses regular expressions rather than a parser to do its work. - /// - private static partial class RegularExpressions - { - /************************************************************************************************************************** - * WARNING: The regular expressions below MUST be kept in sync with the expressions in the ProjectWriter class -- if the - * description of an item vector changes, the expressions must be updated in both places. - *************************************************************************************************************************/ - -#if NET - [GeneratedRegex(ItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture)] - internal static partial Regex ItemMetadataRegex { get; } -#else - /// - /// Regular expression used to match item metadata references embedded in strings. - /// For example, %(Compile.DependsOn) or %(DependsOn). - /// - internal static Regex ItemMetadataRegex => s_itemMetadataRegex ??= - new Regex(ItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.Compiled); - - internal static Regex s_itemMetadataRegex; -#endif - - /// - /// Name of the group matching the "name" of a metadatum. - /// - internal const string NameGroup = "NAME"; - - /// - /// Name of the group matching the prefix on a metadata expression, for example "Compile." in "%(Compile.Object)". - /// - internal const string ItemSpecificationGroup = "ITEM_SPECIFICATION"; - - /// - /// Name of the group matching the item type in an item expression or metadata expression. - /// - internal const string ItemTypeGroup = "ITEM_TYPE"; - - internal const string NonTransformItemMetadataSpecification = @"((?<=" + ItemVectorWithTransformLHS + @")" + ItemMetadataSpecification + @"(?!" + - ItemVectorWithTransformRHS + @")) | ((? - /// regular expression used to match item metadata references outside of item vector transforms. - /// - /// PERF WARNING: this Regex is complex and tends to run slowly. - private static Regex s_nonTransformItemMetadataPattern; - - internal static Regex NonTransformItemMetadataRegex => s_nonTransformItemMetadataPattern ??= - new Regex(NonTransformItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.Compiled); -#endif - - /// - /// Complete description of an item metadata reference, including the optional qualifying item type. - /// For example, %(Compile.DependsOn) or %(DependsOn). - /// - private const string ItemMetadataSpecification = @"%\(\s* (?(?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @")\s*\.\s*)? (?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @") \s*\)"; - - /// - /// description of an item vector with a transform, left hand side. - /// - private const string ItemVectorWithTransformLHS = @"@\(\s*" + ProjectWriter.itemTypeOrMetadataNameSpecification + @"\s*->\s*'[^']*"; - - /// - /// description of an item vector with a transform, right hand side. - /// - private const string ItemVectorWithTransformRHS = @"[^']*'(\s*,\s*'[^']*')?\s*\)"; - - /************************************************************************************************************************** - * WARNING: The regular expressions above MUST be kept in sync with the expressions in the ProjectWriter class. - *************************************************************************************************************************/ - - /// - /// Copied from and modified to use a rather than repeatedly allocating a . This - /// allows us to avoid intermediate string allocations when repeatedly doing replacements. - /// - /// The string to operate on. - /// A function to transform any matches found. - /// State used in the transform function. - /// The that will accumulate the results. - /// The that will perform the matching. - public static void ReplaceAndAppend(string input, Func evaluator, MetadataMatchEvaluator metadataMatchEvaluator, SpanBasedStringBuilder stringBuilder, Regex regex) - { - ReplaceAndAppend(input, evaluator, metadataMatchEvaluator, -1, regex.RightToLeft ? input.Length : 0, stringBuilder, regex); - } - - /// - /// Copied from and modified to use a rather than repeatedly allocating a . This - /// allows us to avoid intermediate string allocations when repeatedly doing replacements. - /// - /// The string to operate on. - /// A function to transform any matches found. - /// State used in the transform function. - /// The number of replacements. - /// Index to start when doing replacements. - /// The that will accumulate the results. - /// The that will perform the matching. - public static void ReplaceAndAppend(string input, Func evaluator, MetadataMatchEvaluator matchEvaluatorState, int count, int startat, SpanBasedStringBuilder stringBuilder, Regex regex) - { - if (evaluator is null) - { - throw new ArgumentNullException(nameof(evaluator)); - } - - if (stringBuilder is null) - { - throw new ArgumentNullException(nameof(stringBuilder)); - } - - if (count < -1) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - if (startat < 0 || startat > input.Length) - { - throw new ArgumentOutOfRangeException(nameof(startat)); - } - - if (regex is null) - { - throw new ArgumentNullException(nameof(regex)); - } - - if (count == 0) - { - stringBuilder.Append(input); - - return; - } - - Match match = regex.Match(input, startat); - if (!match.Success) - { - stringBuilder.Append(input); - - return; - } - - if (!regex.RightToLeft) - { - int prevat = 0; - do - { - if (match.Index != prevat) - { - stringBuilder.Append(input, prevat, match.Index - prevat); - } - - prevat = match.Index + match.Length; - stringBuilder.Append(evaluator(match, matchEvaluatorState)); - if (--count == 0) - { - break; - } - - match = match.NextMatch(); - } - while (match.Success); - if (prevat < input.Length) - { - stringBuilder.Append(input, prevat, input.Length - prevat); - } - } - else - { - List> list = new List>(); - int prevat = input.Length; - do - { - if (match.Index + match.Length != prevat) - { - list.Add(input.AsMemory().Slice(match.Index + match.Length, prevat - match.Index - match.Length)); - } - - prevat = match.Index; - list.Add(evaluator(match, matchEvaluatorState).AsMemory()); - if (--count == 0) - { - break; - } - - match = match.NextMatch(); - } - while (match.Success); - - if (prevat > 0) - { - stringBuilder.Append(input, 0, prevat); - } - - for (int i = list.Count - 1; i >= 0; i--) - { - stringBuilder.Append(list[i]); - } - } - } - } -} diff --git a/src/msbuild/src/Build/Evaluation/ExpressionShredder.cs b/src/msbuild/src/Build/Evaluation/ExpressionShredder.cs index 8928686dc566..09af95e833cc 100644 --- a/src/msbuild/src/Build/Evaluation/ExpressionShredder.cs +++ b/src/msbuild/src/Build/Evaluation/ExpressionShredder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using Microsoft.Build.Collections; using Microsoft.Build.Shared; +using Microsoft.NET.StringTools; #nullable disable @@ -160,7 +161,7 @@ public bool MoveNext() // Grab the name, but continue to verify it's a well-formed expression // before we store it. - string itemName = Microsoft.NET.StringTools.Strings.WeakIntern(expression.AsSpan(startOfName, currentIndex - startOfName)); + string itemName = Strings.WeakIntern(expression.AsSpan(startOfName, currentIndex - startOfName)); SinkWhitespace(expression, ref currentIndex); bool transformOrFunctionFound = true; @@ -258,7 +259,7 @@ public bool MoveNext() // Create an expression capture that encompasses the entire expression between the @( and the ) // with the item name and any separator contained within it // and each transform expression contained within it (i.e. each ->XYZ) - ItemExpressionCapture expressionCapture = new ItemExpressionCapture(startPoint, endPoint - startPoint, Microsoft.NET.StringTools.Strings.WeakIntern(expression.AsSpan(startPoint, endPoint - startPoint)), itemName, separator, separatorStart, transformExpressions); + ItemExpressionCapture expressionCapture = new ItemExpressionCapture(startPoint, endPoint - startPoint, Strings.WeakIntern(expression.AsSpan(startPoint, endPoint - startPoint)), itemName, separator, separatorStart, transformExpressions); Current = expressionCapture; ++currentIndex; @@ -417,65 +418,87 @@ internal static void GetReferencedItemNamesAndMetadata(string expression, int st // formed metadata expression. (Subtract one for the increment when we loop around.) restartPoint = i - 1; - SinkWhitespace(expression, ref i); - - int startOfText = i; - - if (!SinkValidName(expression, ref i, end)) + if (!TryParseMetadataExpression(expression, ref i, end, out string itemName, out string metadataName)) { i = restartPoint; continue; } - // Grab this, but we don't know if it's an item or metadata name yet - string firstPart = expression.Substring(startOfText, i - startOfText); - string itemName = null; - string metadataName; - string qualifiedMetadataName; + if ((whatToShredFor & ShredderOptions.MetadataOutsideTransforms) != 0) + { + string qualifiedMetadataName = itemName != null ? $"{itemName}.{metadataName}" : metadataName; + pair.Metadata ??= new Dictionary(MSBuildNameIgnoreCaseComparer.Default); + pair.Metadata[qualifiedMetadataName] = new MetadataReference(itemName, metadataName); + } - SinkWhitespace(expression, ref i); + // Compensate for the for-loop's i++ since TryParseMetadataExpression + // already advanced i past the closing ')'. + i--; + } + } + } - bool qualified = Sink(expression, ref i, '.'); + /// + /// Attempts to parse a metadata expression of the form %(Name) or %(ItemType.Name), + /// starting just after the %( has been consumed (i.e., points at + /// the first character after the opening parenthesis). + /// + /// + /// On success, is left one past the closing ). + /// On failure, is at an indeterminate position and the caller + /// should restore it from a saved restart point. + /// + /// The expression being scanned. + /// Current scan position (just after %(). Advanced on success. + /// Exclusive end index of the scan range; no character at or beyond this index is read. + /// The item type if qualified; otherwise . + /// The metadata name. + /// + /// if a valid metadata expression was parsed. + /// + internal static bool TryParseMetadataExpression(string expression, ref int i, int end, out string itemType, out string metadataName) + { + itemType = null; + metadataName = null; - if (qualified) - { - SinkWhitespace(expression, ref i); + SinkWhitespace(expression, ref i, end); - startOfText = i; + int startOfText = i; - if (!SinkValidName(expression, ref i, end)) - { - i = restartPoint; - continue; - } + if (!SinkValidName(expression, ref i, end)) + { + return false; + } - itemName = firstPart; - metadataName = expression.Substring(startOfText, i - startOfText); - qualifiedMetadataName = $"{itemName}.{metadataName}"; - } - else - { - metadataName = firstPart; - qualifiedMetadataName = metadataName; - } + string firstName = Strings.WeakIntern(expression.AsSpan(startOfText, i - startOfText)); - SinkWhitespace(expression, ref i); + SinkWhitespace(expression, ref i, end); - if (!Sink(expression, ref i, ')')) - { - i = restartPoint; - continue; - } + if (Sink(expression, ref i, end, '.')) + { + // Qualified: %(ItemType.Name) + itemType = firstName; - if ((whatToShredFor & ShredderOptions.MetadataOutsideTransforms) != 0) - { - pair.Metadata ??= new Dictionary(MSBuildNameIgnoreCaseComparer.Default); - pair.Metadata[qualifiedMetadataName] = new MetadataReference(itemName, metadataName); - } + SinkWhitespace(expression, ref i, end); - i--; + startOfText = i; + + if (!SinkValidName(expression, ref i, end)) + { + return false; } + + metadataName = Strings.WeakIntern(expression.AsSpan(startOfText, i - startOfText)); + + SinkWhitespace(expression, ref i, end); + } + else + { + // Unqualified: %(Name) + metadataName = firstName; } + + return Sink(expression, ref i, end, ')'); } /// @@ -620,7 +643,7 @@ private static bool SinkUntilClosingQuote(char quoteChar, string expression, ref string functionArguments = null; if (endFunctionArguments > startFunctionArguments) { - functionArguments = Microsoft.NET.StringTools.Strings.WeakIntern(expression.AsSpan(startFunctionArguments, endFunctionArguments - startFunctionArguments)); + functionArguments = Strings.WeakIntern(expression.AsSpan(startFunctionArguments, endFunctionArguments - startFunctionArguments)); } ItemExpressionCapture capture = new ItemExpressionCapture(startTransform, i - startTransform, expression.Substring(startTransform, i - startTransform), null, null, -1, null, functionName, functionArguments); @@ -640,6 +663,15 @@ private static bool SinkUntilClosingQuote(char quoteChar, string expression, ref /// Returns true if a valid name begins at the specified index. /// Leaves index one past the end of the name. /// + /// + /// The accepted grammar is [A-Za-z_][A-Za-z_0-9\-]* (via + /// and + /// ), which defines a valid item + /// type or metadata name. This MUST be kept in sync with + /// : if the grammar used to parse + /// item/metadata expressions diverges from the one used to write them back out, expressions could + /// round-trip incorrectly. + /// private static bool SinkValidName(string expression, ref int i, int end) { if (end <= i || !XmlUtilities.IsValidInitialElementNameCharacter(expression[i])) @@ -658,13 +690,19 @@ private static bool SinkValidName(string expression, ref int i, int end) } /// - /// Returns true if the character at the specified index - /// is the specified char. - /// Leaves index one past the character. + /// Returns if the character at the specified index is the specified char. + /// Leaves index one past the character. /// private static bool Sink(string expression, ref int i, char c) + => Sink(expression, ref i, expression.Length, c); + + /// + /// Returns if the character at the specified index (which must be before + /// ) is the specified char. Leaves index one past the character. + /// + private static bool Sink(string expression, ref int i, int end, char c) { - if (i < expression.Length && expression[i] == c) + if (i < end && expression[i] == c) { i++; return true; @@ -674,9 +712,8 @@ private static bool Sink(string expression, ref int i, char c) } /// - /// Returns true if the next two characters at the specified index - /// are the specified sequence. - /// Leaves index one past the second character. + /// Returns if the next two characters at the specified index are the specified sequence. + /// Leaves index one past the second character. /// private static bool Sink(string expression, ref int i, int end, char c1, char c2) { @@ -690,18 +727,34 @@ private static bool Sink(string expression, ref int i, int end, char c1, char c2 } /// - /// Moves past all whitespace starting at the specified index. - /// Returns the next index, possibly the string length. + /// Moves past all whitespace starting at the specified index. + /// Returns the next index, possibly the string length. /// - /// - /// Char.IsWhitespace() is not identical in behavior to regex's \s character class, - /// but it's extremely close, and it's what we use in conditional expressions. - /// /// The expression to process. /// The start location for skipping whitespace, contains the next non-whitespace character on exit. + /// + /// is not identical in behavior to regex's \s character class, + /// but it's extremely close, and it's what we use in conditional expressions. + /// private static void SinkWhitespace(string expression, ref int i) + => SinkWhitespace(expression, ref i, expression.Length); + + /// + /// Moves past all whitespace starting at the specified index, without scanning at or beyond + /// . Returns the next index, possibly . + /// + /// The expression to process. + /// + /// The start location for skipping whitespace, contains the next non-whitespace character (or ) on exit. + /// + /// Exclusive end index of the scan range. + /// + /// is not identical in behavior to regex's \s character class, + /// but it's extremely close, and it's what we use in conditional expressions. + /// + private static void SinkWhitespace(string expression, ref int i, int end) { - while (i < expression.Length && Char.IsWhiteSpace(expression[i])) + while (i < end && char.IsWhiteSpace(expression[i])) { i++; } diff --git a/src/msbuild/src/Build/Microsoft.Build.csproj b/src/msbuild/src/Build/Microsoft.Build.csproj index b0084bee1b72..eaa7400b4bd7 100644 --- a/src/msbuild/src/Build/Microsoft.Build.csproj +++ b/src/msbuild/src/Build/Microsoft.Build.csproj @@ -191,9 +191,7 @@ - - diff --git a/src/msbuild/src/Build/Utilities/ProjectWriter.cs b/src/msbuild/src/Build/Utilities/ProjectWriter.cs index 9f0372433192..4cc35a38ae11 100644 --- a/src/msbuild/src/Build/Utilities/ProjectWriter.cs +++ b/src/msbuild/src/Build/Utilities/ProjectWriter.cs @@ -25,6 +25,8 @@ internal sealed partial class ProjectWriter : XmlTextWriter // the portion of the expression that matches the item type or metadata name, eg: "foo123" // Note that the pattern is more strict than the rules for valid XML element names. + // This grammar MUST be kept in sync with ExpressionShredder.SinkValidName, which validates + // item type and metadata names when parsing item/metadata expressions. internal const string itemTypeOrMetadataNameSpecification = @"[A-Za-z_][A-Za-z_0-9\-]*"; // regular expression used to match item vector transforms diff --git a/src/msbuild/src/Framework.UnitTests/Assumed_Tests.cs b/src/msbuild/src/Framework.UnitTests/Assumed_Tests.cs index 009f2f962225..9117a0db88b1 100644 --- a/src/msbuild/src/Framework.UnitTests/Assumed_Tests.cs +++ b/src/msbuild/src/Framework.UnitTests/Assumed_Tests.cs @@ -93,6 +93,12 @@ public void True_DoesNotThrow_WhenTrue() Assumed.True(true); } + [Fact] + public void True_DoesNotThrow_WithMessage_WhenTrue() + { + Assumed.True(true, "msbuild rules"); + } + [Fact] public void True_Throws_WhenFalse() { @@ -108,6 +114,38 @@ public void True_InterpolatedHandler_Throws_WhenFalse() }); } + [Fact] + public void True_InterpolatedHandler_DoesNotFormat_WhenConditionIsTrue() + { + bool formatted = false; + Assumed.True(true, $"message {FormatSideEffect(ref formatted)}"); + formatted.ShouldBeFalse("Interpolated string should not have been formatted when condition is true"); + } + + [Fact] + public void True_InterpolatedHandler_FormatsMessage_WhenConditionIsFalse() + { + bool formatted = false; + var ex = Should.Throw(() => + { + Assumed.True(false, $"error: {FormatSideEffect(ref formatted)}"); + }); + + formatted.ShouldBeTrue("Interpolated string should have been formatted when condition is false"); + ex.Message.ShouldContain("error: formatted"); + } + + [Fact] + public void True_InterpolatedHandler_FormatsMultipleArgs_WhenConditionIsFalse() + { + var ex = Should.Throw(() => + { + Assumed.True(false, $"a={1} b={2} c={"three"}"); + }); + + ex.Message.ShouldContain("a=1 b=2 c=three"); + } + [Fact] public void False_DoesNotThrow_WhenFalse() { @@ -716,4 +754,10 @@ public void NotNullOrEmpty_Collection_WithDefaultMessage_IncludesExpression() var ex = Should.Throw(() => Assumed.NotNullOrEmpty(myCollection)); ex.Message.ShouldContain("myCollection"); } + + private static string FormatSideEffect(ref bool formatted) + { + formatted = true; + return "formatted"; + } } diff --git a/src/msbuild/src/MSBuild.Benchmarks/ExpanderBenchmark.cs b/src/msbuild/src/MSBuild.Benchmarks/ExpanderBenchmark.cs new file mode 100644 index 000000000000..beeb4bb27a63 --- /dev/null +++ b/src/msbuild/src/MSBuild.Benchmarks/ExpanderBenchmark.cs @@ -0,0 +1,248 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BenchmarkDotNet.Attributes; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.Shared.FileSystem; + +namespace MSBuild.Benchmarks; + +/// +/// Benchmarks for the covering property expansion, +/// item list expansion, metadata expansion, and mixed expressions. +/// +[MemoryDiagnoser] +public class ExpanderBenchmark +{ + /// + /// Number of properties to populate in the property bag. + /// + [Params(10, 100)] + public int PropertyCount { get; set; } + + /// + /// Number of items per item type in the item dictionary. + /// + [Params(10, 100)] + public int ItemCount { get; set; } + + private Expander _expander = null!; + private IElementLocation _location = null!; + + // Kept alive for the lifetime of the benchmark because the created ProjectInstance/ + // ProjectItemInstance objects hold references back to this collection. Disposed in GlobalCleanup. + private ProjectCollection _projectCollection = null!; + + // Pre-built expression strings, assigned in GlobalSetup. + private string _singleProperty = null!; + private string _multipleProperties = null!; + private string _nestedProperties = null!; + private string _propertyConcat = null!; + + private string _singleItemList = null!; + private string _itemListWithTransform = null!; + private string _itemListWithSeparator = null!; + + private string _singleMetadata = null!; + private string _qualifiedMetadata = null!; + private string _multipleMetadata = null!; + + private string _mixedPropertyAndItem = null!; + private string _mixedPropertyAndMetadata = null!; + private string _mixedAll = null!; + + private string _noExpansion = null!; + + [GlobalSetup] + public void GlobalSetup() + { + _location = ElementLocation.EmptyLocation; + + // Use a dedicated ProjectCollection so the benchmark does not leak state into the global one. + // Stored in a field (not a local using) so it stays alive for the benchmark iterations; the + // ProjectInstance/ProjectItemInstance objects below reference it. Disposed in GlobalCleanup. + _projectCollection = new ProjectCollection(); + ProjectRootElement xml = ProjectRootElement.Create(_projectCollection); + xml.FullPath = Path.Combine(Path.GetTempPath(), "project.csproj"); + ProjectInstance project = new(xml, globalProperties: null, toolsVersion: null, _projectCollection); + + // --- Properties --- + var properties = new PropertyDictionary(); + for (int i = 0; i < PropertyCount; i++) + { + properties.Set(ProjectPropertyInstance.Create($"Prop{i}", $"Value{i}")); + } + + // Add well-known properties used in expressions. + properties.Set(ProjectPropertyInstance.Create("Configuration", "Release")); + properties.Set(ProjectPropertyInstance.Create("Platform", "AnyCPU")); + properties.Set(ProjectPropertyInstance.Create("OutputPath", @"bin\Release\net10.0")); + properties.Set(ProjectPropertyInstance.Create("RootNamespace", "MyProject.Core")); + properties.Set(ProjectPropertyInstance.Create("AssemblyName", "MyProject.Core")); + properties.Set(ProjectPropertyInstance.Create("TargetFramework", "net10.0")); + + // --- Items --- + var itemBag = new ItemDictionary(); + for (int i = 0; i < ItemCount; i++) + { + var item = new ProjectItemInstance(project, "Compile", $@"src\dir{i % 10}\File{i}.cs", project.FullPath); + item.SetMetadata("Culture", i % 2 == 0 ? "en-US" : "fr-FR"); + item.SetMetadata("Link", $@"linked\File{i}.cs"); + item.SetMetadata("Generator", "ResXFileCodeGenerator"); + itemBag.Add(item); + } + + for (int i = 0; i < ItemCount / 2; i++) + { + var item = new ProjectItemInstance(project, "Reference", $"System.Lib{i}", project.FullPath); + item.SetMetadata("HintPath", $@"packages\lib{i}\lib\net10.0\System.Lib{i}.dll"); + item.SetMetadata("Private", "true"); + itemBag.Add(item); + } + + // --- Metadata table (for unqualified/qualified metadata lookups) --- + var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Culture"] = "en-US", + ["Generator"] = "ResXFileCodeGenerator", + ["Compile.Link"] = @"linked\SomeFile.cs", + ["Compile.Culture"] = "de-DE", + ["Identity"] = @"src\SomeFile.cs", + }; + + var metadataTable = new StringMetadataTable(metadata); + + _expander = new Expander( + properties, itemBag, metadataTable, FileSystems.Default); + + // --- Build expressions --- + + // Property expressions + _singleProperty = "$(Configuration)"; + _multipleProperties = @"$(Configuration)\$(Platform)\$(OutputPath)"; + _nestedProperties = "$(RootNamespace).$(AssemblyName)"; + _propertyConcat = $"prefix_$(Configuration)_$(Platform)_$(TargetFramework)_suffix"; + + // Item expressions + _singleItemList = "@(Compile)"; + _itemListWithTransform = "@(Compile->'%(Filename).obj')"; + _itemListWithSeparator = "@(Compile, ',')"; + + // Metadata expressions + _singleMetadata = "%(Culture)"; + _qualifiedMetadata = "%(Compile.Link)"; + _multipleMetadata = "%(Culture)_%(Generator)"; + + // Mixed expressions + _mixedPropertyAndItem = @"$(OutputPath)\@(Compile->'%(Filename)')"; + _mixedPropertyAndMetadata = @"$(OutputPath)\%(Culture)\%(Identity)"; + _mixedAll = @"$(OutputPath)\%(Culture)\@(Compile->'%(Filename)')"; + + // Plain string (no expansion needed) + _noExpansion = @"This is a plain string with no expansion tokens at all."; + } + + [GlobalCleanup] + public void GlobalCleanup() + => _projectCollection?.Dispose(); + + // ========================================================================= + // Property expansion + // ========================================================================= + + [Benchmark] + public string Property_Single() + => _expander.ExpandIntoStringLeaveEscaped(_singleProperty, ExpanderOptions.ExpandProperties, _location); + + [Benchmark] + public string Property_Multiple() + => _expander.ExpandIntoStringLeaveEscaped(_multipleProperties, ExpanderOptions.ExpandProperties, _location); + + [Benchmark] + public string Property_Nested() + => _expander.ExpandIntoStringLeaveEscaped(_nestedProperties, ExpanderOptions.ExpandProperties, _location); + + [Benchmark] + public string Property_Concatenation() + => _expander.ExpandIntoStringLeaveEscaped(_propertyConcat, ExpanderOptions.ExpandProperties, _location); + + // ========================================================================= + // Item list expansion + // ========================================================================= + + [Benchmark] + public string ItemList_Simple() + => _expander.ExpandIntoStringLeaveEscaped(_singleItemList, ExpanderOptions.ExpandItems, _location); + + [Benchmark] + public string ItemList_WithTransform() + => _expander.ExpandIntoStringLeaveEscaped(_itemListWithTransform, ExpanderOptions.ExpandItems, _location); + + [Benchmark] + public string ItemList_WithSeparator() + => _expander.ExpandIntoStringLeaveEscaped(_itemListWithSeparator, ExpanderOptions.ExpandItems, _location); + + // ========================================================================= + // Metadata expansion + // ========================================================================= + + [Benchmark] + public string Metadata_Unqualified() + => _expander.ExpandIntoStringLeaveEscaped(_singleMetadata, ExpanderOptions.ExpandMetadata, _location); + + [Benchmark] + public string Metadata_Qualified() + => _expander.ExpandIntoStringLeaveEscaped(_qualifiedMetadata, ExpanderOptions.ExpandMetadata, _location); + + [Benchmark] + public string Metadata_Multiple() + => _expander.ExpandIntoStringLeaveEscaped(_multipleMetadata, ExpanderOptions.ExpandMetadata, _location); + + // ========================================================================= + // Mixed expansion + // ========================================================================= + + [Benchmark] + public string Mixed_PropertyAndItem() + => _expander.ExpandIntoStringLeaveEscaped(_mixedPropertyAndItem, ExpanderOptions.ExpandPropertiesAndItems, _location); + + [Benchmark] + public string Mixed_PropertyAndMetadata() + => _expander.ExpandIntoStringLeaveEscaped(_mixedPropertyAndMetadata, ExpanderOptions.ExpandPropertiesAndMetadata, _location); + + [Benchmark] + public string Mixed_All() + => _expander.ExpandIntoStringLeaveEscaped(_mixedAll, ExpanderOptions.ExpandAll, _location); + + // ========================================================================= + // Baseline: no expansion + // ========================================================================= + + [Benchmark(Baseline = true)] + public string NoExpansion() + => _expander.ExpandIntoStringLeaveEscaped(_noExpansion, ExpanderOptions.ExpandAll, _location); + + // ========================================================================= + // ExpandIntoStringAndUnescape variants (measures unescape overhead) + // ========================================================================= + + [Benchmark] + public string PropertyAndUnescape_Multiple() + => _expander.ExpandIntoStringAndUnescape(_multipleProperties, ExpanderOptions.ExpandProperties, _location); + + [Benchmark] + public string ItemListAndUnescape_WithTransform() + => _expander.ExpandIntoStringAndUnescape(_itemListWithTransform, ExpanderOptions.ExpandItems, _location); + + [Benchmark] + public string MetadataAndUnescape_Multiple() + => _expander.ExpandIntoStringAndUnescape(_multipleMetadata, ExpanderOptions.ExpandMetadata, _location); + + [Benchmark] + public string MixedAndUnescape_All() + => _expander.ExpandIntoStringAndUnescape(_mixedAll, ExpanderOptions.ExpandAll, _location); +} diff --git a/src/msbuild/src/Package/MSBuild.VSSetup/files.swr b/src/msbuild/src/Package/MSBuild.VSSetup/files.swr index 48e07799337b..653d8b9d1a92 100644 --- a/src/msbuild/src/Package/MSBuild.VSSetup/files.swr +++ b/src/msbuild/src/Package/MSBuild.VSSetup/files.swr @@ -33,7 +33,7 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)Microsoft.IO.Redist.dll vs.file.ngenApplications="[installDir]\Common7\IDE\vsn.exe" vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=2 file source=$(X86BinPath)MSBuild.exe vs.file.ngenArchitecture=x86 vs.file.ngenPriority=2 file source=$(X86BinPath)MSBuild.exe.config - file source=$(CoordinatorBinPath)MSBuild.Coordinator.exe vs.file.ngenArchitecture=x86 vs.file.ngenPriority=3 + file source=$(CoordinatorBinPath)MSBuild.Coordinator.exe file source=$(CoordinatorBinPath)MSBuild.Coordinator.exe.config file source=$(TaskHostBinPath)MSBuildTaskHost.exe file source=$(TaskHostBinPath)MSBuildTaskHost.exe.config diff --git a/src/msbuild/src/Shared/UnitTests/ErrorUtilities_Tests.cs b/src/msbuild/src/Shared/UnitTests/ErrorUtilities_Tests.cs deleted file mode 100644 index 9969720bd800..000000000000 --- a/src/msbuild/src/Shared/UnitTests/ErrorUtilities_Tests.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Build.Framework; -using Microsoft.Build.Shared; -using Xunit; - -namespace Microsoft.Build.UnitTests; - -public sealed class ErrorUtilities_Tests -{ - [Fact] - public void VerifyThrowFalse() - { - var ex = Assert.Throws(() => - { - Assumed.True(false, "msbuild rules"); - }); - - Assert.Contains("msbuild rules", ex.Message); - } - - [Fact] - public void VerifyThrowTrue() - { - // This shouldn't throw. - Assumed.True(true, "msbuild rules"); - } - - [Fact] - public void VerifyThrow_InterpolatedString_DoesNotFormat_WhenConditionIsTrue() - { - bool formatted = false; - Assumed.True(true, $"message {FormatSideEffect(ref formatted)}"); - Assert.False(formatted, "Interpolated string should not have been formatted when condition is true"); - } - - [Fact] - public void VerifyThrow_InterpolatedString_Formats_WhenConditionIsFalse() - { - bool formatted = false; - var ex = Assert.Throws(() => - { - Assumed.True(false, $"error: {FormatSideEffect(ref formatted)}"); - }); - - Assert.True(formatted, "Interpolated string should have been formatted when condition is false"); - Assert.Contains("error: formatted", ex.Message); - } - - [Fact] - public void VerifyThrow_InterpolatedString_FormatsMultipleArgs_WhenConditionIsFalse() - { - var ex = Assert.Throws(() => - { - Assumed.True(false, $"a={1} b={2} c={"three"}"); - }); - - Assert.Contains("a=1 b=2 c=three", ex.Message); - } - - private static string FormatSideEffect(ref bool formatted) - { - formatted = true; - return "formatted"; - } -} diff --git a/src/msbuild/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj b/src/msbuild/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj index d46ce18147bb..50b7fdc3cdc2 100644 --- a/src/msbuild/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj +++ b/src/msbuild/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj @@ -45,7 +45,6 @@ - NativeMethodsShared_Tests.cs diff --git a/src/msbuild/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj b/src/msbuild/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj index 07580c65e795..2f1162521568 100644 --- a/src/msbuild/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj +++ b/src/msbuild/src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj @@ -25,7 +25,6 @@ - diff --git a/src/source-manifest.json b/src/source-manifest.json index 2d818d1d80e0..ba69b690d8e2 100644 --- a/src/source-manifest.json +++ b/src/source-manifest.json @@ -55,10 +55,10 @@ "commitSha": "7408e8f8e7f8926412846a9d0c6c2d276c67acd4" }, { - "barId": 319885, + "barId": 320071, "path": "msbuild", "remoteUri": "https://github.com/dotnet/msbuild", - "commitSha": "a47a64a9382828e968c3564d11f4e0afe99dc526" + "commitSha": "33d17672fac4c26656a201774f9894dff51a4e78" }, { "barId": 319303,