diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddFrameworkReference.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddFrameworkReference.cs new file mode 100644 index 00000000000..5687f15d3f0 --- /dev/null +++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddFrameworkReference.cs @@ -0,0 +1,58 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using OpenRewrite.Core; +using OpenRewrite.Xml; +using ExecutionContext = OpenRewrite.Core.ExecutionContext; + +namespace OpenRewrite.CSharp.Recipes; + +///

+/// Adds a <FrameworkReference> to a .csproj file's project root if one +/// with a matching Include doesn't already exist. The reference is placed in a +/// dedicated <ItemGroup> appended to the project. No-op when the SDK +/// is Microsoft.NET.Sdk.Web, which already imports +/// Microsoft.AspNetCore.App implicitly. +/// +public class AddFrameworkReference : ScanningRecipe +{ + public override string DisplayName => "Add framework reference"; + + public override string Description => + "Adds a `` to a .csproj if it isn't already present."; + + [Option(DisplayName = "Framework name", + Description = "The shared framework name to reference.", + Example = "Microsoft.AspNetCore.App")] + public string FrameworkName { get; set; } = ""; + + [Option(DisplayName = "Trigger package glob", + Description = "Optional glob: only add the framework reference when a `` " + + "matching this glob is present in the project. Leave blank to always add.", + Example = "Microsoft.AspNetCore.*", + Required = false)] + public string? TriggerPackageGlob { get; set; } + + public override DotNetBuildContext GetInitialValue(ExecutionContext ctx) => DotNetBuildContext.GetOrCreate(ctx); + + public override ITreeVisitor GetScanner(DotNetBuildContext acc) => new BuildContextScanner(); + + public override ITreeVisitor GetVisitor(DotNetBuildContext acc) + { + return Preconditions.Check( + new IsProjectFile(), + new AddFrameworkReferenceVisitor(FrameworkName, TriggerPackageGlob)); + } +} diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddFrameworkReferenceVisitor.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddFrameworkReferenceVisitor.cs new file mode 100644 index 00000000000..696055347f0 --- /dev/null +++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddFrameworkReferenceVisitor.cs @@ -0,0 +1,85 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using OpenRewrite.Core; +using OpenRewrite.Xml; +using ExecutionContext = OpenRewrite.Core.ExecutionContext; + +namespace OpenRewrite.CSharp.Recipes; + +///

+/// Visitor that adds a <FrameworkReference> to a .csproj file root if +/// one with a matching Include doesn't already exist. Skips projects whose Sdk +/// attribute already implicitly imports the same framework +/// (Microsoft.NET.Sdk.Web implicitly references +/// Microsoft.AspNetCore.App). +/// +public class AddFrameworkReferenceVisitor(string frameworkName, string? triggerPackageGlob = null) : XmlVisitor +{ + private bool _alreadyPresent; + private bool _triggerMatched; + + public override Xml.Xml VisitDocument(Document document, ExecutionContext ctx) + { + _alreadyPresent = false; + _triggerMatched = string.IsNullOrEmpty(triggerPackageGlob); + + // Skip when the SDK already imports the same framework implicitly. + var sdk = document.Root.GetAttributeValue("Sdk"); + if (sdk != null && SdkImplicitlyReferences(sdk, frameworkName)) + return document; + + var d = (Document)base.VisitDocument(document, ctx); + + if (_alreadyPresent || !_triggerMatched) + return d; + + var tag = $""; + var itemGroup = TagExtensions.BuildTag( + $"\n {tag}\n "); + DoAfterVisit(new AddToTagVisitor(d.Root, itemGroup)); + DoAfterVisit(MSBuildProjectHelper.RegenerateMarkerVisitor()); + return d; + } + + public override Xml.Xml VisitTag(Tag tag, ExecutionContext ctx) + { + var t = (Tag)base.VisitTag(tag, ctx); + if (t.Name == "FrameworkReference") + { + var include = t.GetAttributeValue("Include"); + if (include == frameworkName) + _alreadyPresent = true; + } + else if (!string.IsNullOrEmpty(triggerPackageGlob) && t.Name == "PackageReference") + { + var include = t.GetAttributeValue("Include"); + if (include != null && GlobMatcher.Matches(include, triggerPackageGlob!)) + _triggerMatched = true; + } + return t; + } + + private static bool SdkImplicitlyReferences(string sdk, string framework) + { + return sdk switch + { + "Microsoft.NET.Sdk.Web" => framework is "Microsoft.AspNetCore.App" or "Microsoft.NETCore.App", + "Microsoft.NET.Sdk.Worker" => framework is "Microsoft.AspNetCore.App" or "Microsoft.NETCore.App", + "Microsoft.NET.Sdk.BlazorWebAssembly" => framework is "Microsoft.AspNetCore.App" or "Microsoft.NETCore.App", + _ => false + }; + } +} diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/CsprojRecipeActivator.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/CsprojRecipeActivator.cs index 36ee3bdbcc3..d2b5a029c45 100644 --- a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/CsprojRecipeActivator.cs +++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/CsprojRecipeActivator.cs @@ -28,5 +28,8 @@ public void Activate(RecipeMarketplace marketplace) marketplace.Install(new UpgradeNuGetPackageVersion(), CsprojCategory); marketplace.Install(new ChangeDotNetTargetFramework(), CsprojCategory); marketplace.Install(new FindNuGetPackageReference(), CsprojCategory); + marketplace.Install(new RemoveMSBuildProperty(), CsprojCategory); + marketplace.Install(new RemoveDotNetCliToolReference(), CsprojCategory); + marketplace.Install(new AddFrameworkReference(), CsprojCategory); } } diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveDotNetCliToolReference.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveDotNetCliToolReference.cs new file mode 100644 index 00000000000..e6106676e7f --- /dev/null +++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveDotNetCliToolReference.cs @@ -0,0 +1,53 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using OpenRewrite.Core; +using OpenRewrite.Xml; +using ExecutionContext = OpenRewrite.Core.ExecutionContext; + +namespace OpenRewrite.CSharp.Recipes; + +///

+/// Removes <DotNetCliToolReference> entries (matching by glob on the +/// Include attribute) from .csproj files. CLI tool references are obsolete starting +/// with .NET Core 3.0 — they were the netcoreapp2.x mechanism for shipping per-project +/// CLI tools, and have since been replaced by global / local tools and SDK-built-in +/// commands (e.g. dotnet watch). +/// +public class RemoveDotNetCliToolReference : ScanningRecipe +{ + public override string DisplayName => "Remove DotNetCliToolReference"; + + public override string Description => + "Removes a `` element from .csproj files. " + + "Use `*` to remove every CLI tool reference."; + + [Option(DisplayName = "Tool name", + Description = "The CLI tool package name to remove. Supports glob patterns. " + + "Use `*` to remove all CLI tool references.", + Example = "Microsoft.DotNet.Watcher.Tools")] + public string ToolName { get; set; } = ""; + + public override DotNetBuildContext GetInitialValue(ExecutionContext ctx) => DotNetBuildContext.GetOrCreate(ctx); + + public override ITreeVisitor GetScanner(DotNetBuildContext acc) => new BuildContextScanner(); + + public override ITreeVisitor GetVisitor(DotNetBuildContext acc) + { + return Preconditions.Check( + new IsProjectFile(), + new RemoveDotNetCliToolReferenceVisitor(ToolName)); + } +} diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveDotNetCliToolReferenceVisitor.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveDotNetCliToolReferenceVisitor.cs new file mode 100644 index 00000000000..ae9c46d39b3 --- /dev/null +++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveDotNetCliToolReferenceVisitor.cs @@ -0,0 +1,53 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using OpenRewrite.Core; +using OpenRewrite.Xml; +using ExecutionContext = OpenRewrite.Core.ExecutionContext; + +namespace OpenRewrite.CSharp.Recipes; + +///

+/// Visitor that removes a <DotNetCliToolReference> element from +/// .csproj files. Supports glob patterns for the tool name. +/// +public class RemoveDotNetCliToolReferenceVisitor(string toolName) : XmlVisitor +{ + private bool _modified; + + public override Xml.Xml VisitDocument(Document document, ExecutionContext ctx) + { + _modified = false; + var d = (Document)base.VisitDocument(document, ctx); + if (_modified) + DoAfterVisit(MSBuildProjectHelper.RegenerateMarkerVisitor()); + return d; + } + + public override Xml.Xml VisitTag(Tag tag, ExecutionContext ctx) + { + var t = (Tag)base.VisitTag(tag, ctx); + if (t.Name == "DotNetCliToolReference") + { + var include = t.GetAttributeValue("Include"); + if (include != null && GlobMatcher.Matches(include, toolName)) + { + _modified = true; + DoAfterVisit(new RemoveContentVisitor(t, true, false)); + } + } + return t; + } +} diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveMSBuildProperty.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveMSBuildProperty.cs new file mode 100644 index 00000000000..fb996f0dd83 --- /dev/null +++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveMSBuildProperty.cs @@ -0,0 +1,50 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using OpenRewrite.Core; +using OpenRewrite.Xml; +using ExecutionContext = OpenRewrite.Core.ExecutionContext; + +namespace OpenRewrite.CSharp.Recipes; + +///

+/// Removes an MSBuild property element (e.g. RuntimeFrameworkVersion) from a +/// PropertyGroup in .csproj files. Useful for stripping legacy properties that +/// are no longer applicable after upgrading the target framework. +/// +public class RemoveMSBuildProperty : ScanningRecipe +{ + public override string DisplayName => "Remove MSBuild property"; + + public override string Description => + "Removes an MSBuild property element (e.g. ``) from " + + "`` in .csproj files."; + + [Option(DisplayName = "Property name", + Description = "The MSBuild property element name to remove (case-sensitive).", + Example = "RuntimeFrameworkVersion")] + public string PropertyName { get; set; } = ""; + + public override DotNetBuildContext GetInitialValue(ExecutionContext ctx) => DotNetBuildContext.GetOrCreate(ctx); + + public override ITreeVisitor GetScanner(DotNetBuildContext acc) => new BuildContextScanner(); + + public override ITreeVisitor GetVisitor(DotNetBuildContext acc) + { + return Preconditions.Check( + new IsProjectFile(), + new RemoveMSBuildPropertyVisitor(PropertyName)); + } +} diff --git a/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveMSBuildPropertyVisitor.cs b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveMSBuildPropertyVisitor.cs new file mode 100644 index 00000000000..0c315028689 --- /dev/null +++ b/rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/RemoveMSBuildPropertyVisitor.cs @@ -0,0 +1,63 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using OpenRewrite.Core; +using OpenRewrite.Xml; +using ExecutionContext = OpenRewrite.Core.ExecutionContext; + +namespace OpenRewrite.CSharp.Recipes; + +///

+/// Visitor that removes an MSBuild property element nested inside a +/// PropertyGroup in .csproj files. Can be used standalone in custom recipe +/// edit phases. +/// +public class RemoveMSBuildPropertyVisitor(string propertyName) : XmlVisitor +{ + private bool _modified; + + public override Xml.Xml VisitDocument(Document document, ExecutionContext ctx) + { + _modified = false; + var d = (Document)base.VisitDocument(document, ctx); + if (_modified) + DoAfterVisit(MSBuildProjectHelper.RegenerateMarkerVisitor()); + return d; + } + + public override Xml.Xml VisitTag(Tag tag, ExecutionContext ctx) + { + var t = (Tag)base.VisitTag(tag, ctx); + if (t.Name == propertyName && IsInPropertyGroup()) + { + _modified = true; + DoAfterVisit(new RemoveContentVisitor(t, true, false)); + } + return t; + } + + private bool IsInPropertyGroup() + { + // Walk parents up to find the enclosing element. + var parent = Cursor.Parent; + while (parent != null) + { + if (parent.Value is Tag pTag) + return pTag.Name == "PropertyGroup"; + parent = parent.Parent; + } + return false; + } +} diff --git a/rewrite-csharp/csharp/OpenRewrite/Test/RewriteTest.cs b/rewrite-csharp/csharp/OpenRewrite/Test/RewriteTest.cs index 80308f9166b..b39f4ea6ee5 100644 --- a/rewrite-csharp/csharp/OpenRewrite/Test/RewriteTest.cs +++ b/rewrite-csharp/csharp/OpenRewrite/Test/RewriteTest.cs @@ -86,13 +86,17 @@ protected void RewriteRun(Action configure, params SourceSpec[] spec foreach (var spec in specs) { SourceFile source; + var isCsFile = spec.SourcePath != null && + spec.SourcePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase); if (spec.SourcePath != null && IsCsprojPath(spec.SourcePath) && csprojParsed != null) { source = csprojParsed[spec.SourcePath]; } - else if (spec.SourcePath != null) + else if (spec.SourcePath != null && !isCsFile) { - // Remote-parsed source (e.g., XML via Java RPC) + // Remote-parsed source (e.g., XML via Java RPC). C# files always use local + // parsing — even with a custom source path — because the Java peer doesn't + // ship a C# parser. var rpc = RewriteRpcServer.Current ?? throw new InvalidOperationException( $"Parsing {spec.SourcePath} requires an RPC connection. " + @@ -103,10 +107,11 @@ protected void RewriteRun(Action configure, params SourceSpec[] spec else { // Local C# parsing + var localSourcePath = spec.SourcePath ?? "source.cs"; SemanticModel? semanticModel = null; if (metadataReferences != null) { - var syntaxTree = CSharpSyntaxTree.ParseText(spec.Before, path: "source.cs"); + var syntaxTree = CSharpSyntaxTree.ParseText(spec.Before, path: localSourcePath); var compilation = CSharpCompilation.Create("TestCompilation") .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) .AddReferences(metadataReferences) @@ -114,7 +119,7 @@ protected void RewriteRun(Action configure, params SourceSpec[] spec semanticModel = compilation.GetSemanticModel(syntaxTree); } - source = parser.Parse(spec.Before, semanticModel: semanticModel); + source = parser.Parse(spec.Before, sourcePath: localSourcePath, semanticModel: semanticModel); // Verify no non-whitespace content leaked into Space fields if (validations.WhitespaceInSpaces) diff --git a/rewrite-csharp/csharp/OpenRewrite/Test/RpcFixture.cs b/rewrite-csharp/csharp/OpenRewrite/Test/RpcFixture.cs index b72d7cd0c69..3cf80836b85 100644 --- a/rewrite-csharp/csharp/OpenRewrite/Test/RpcFixture.cs +++ b/rewrite-csharp/csharp/OpenRewrite/Test/RpcFixture.cs @@ -74,11 +74,7 @@ public void Reset() private static ProcessStartInfo CreateJavaProcessStartInfo() { - var cpFile = Environment.GetEnvironmentVariable("RPC_TEST_SERVER_CLASSPATH") - ?? throw new InvalidOperationException( - "RPC_TEST_SERVER_CLASSPATH environment variable not set. " + - "Run './gradlew :rewrite-csharp:rpcTestClasspath' to generate the classpath file."); - + var cpFile = ResolveClasspathFile(); var classpath = File.ReadAllText(cpFile).Trim(); var psi = new ProcessStartInfo("java", "org.openrewrite.maven.rpc.JavaRewriteRpc") { @@ -92,6 +88,100 @@ private static ProcessStartInfo CreateJavaProcessStartInfo() return psi; } + /// + /// Resolves the rewrite-csharp RPC test server classpath file. + ///

+ /// Order of resolution: + /// 1. RPC_TEST_SERVER_CLASSPATH env var, if it points at an existing file + /// whose mtime isn't older than the SDK assembly that's currently loaded. + /// A stale env var (pointing at an unrelated rewrite checkout's leftover + /// classpath) silently linking the test process to outdated Java JARs is + /// a recurring source of cryptic "recipe didn't fire" failures. + /// 2. The classpath file at a deterministic location relative to the loaded + /// OpenRewrite SDK assembly: walk up from the assembly until we hit + /// rewrite-csharp/csharp/OpenRewrite/{bin,obj}, then the classpath + /// file is at rewrite-csharp/build/rpc-test-server-classpath.txt. + ///

+ private static string ResolveClasspathFile() + { + var sdkRelativeFile = AutoLocateClasspathFile(); + var envFile = Environment.GetEnvironmentVariable("RPC_TEST_SERVER_CLASSPATH"); + + // Prefer the SDK-relative file when it exists and is at least as fresh as + // any env-pointed file — guards against a stale env var inherited from a + // different rewrite checkout. + if (sdkRelativeFile != null && File.Exists(sdkRelativeFile)) + { + if (string.IsNullOrEmpty(envFile) || !File.Exists(envFile) || + File.GetLastWriteTimeUtc(sdkRelativeFile) >= + File.GetLastWriteTimeUtc(envFile)) + { + return sdkRelativeFile; + } + } + + if (!string.IsNullOrEmpty(envFile) && File.Exists(envFile)) + return envFile; + + if (sdkRelativeFile != null && File.Exists(sdkRelativeFile)) + return sdkRelativeFile; + + throw new InvalidOperationException( + "Cannot locate the Java RPC test server classpath. Either set " + + "RPC_TEST_SERVER_CLASSPATH, or run " + + "`./gradlew :rewrite-csharp:rpcTestClasspath` from the rewrite SDK " + + "checkout. Searched SDK-relative path: " + + (sdkRelativeFile ?? "(could not derive from loaded SDK assembly)")); + } + + private static string? AutoLocateClasspathFile() + { + // Two layout cases to handle: + // (a) Test runs INSIDE the rewrite SDK repo: walk up from the SDK assembly + // until we find `/rewrite-csharp/csharp/OpenRewrite/`. + // (b) Test runs in a CONSUMING project (e.g. recipes-csharp) that pulls in + // the SDK via ProjectReference. The DLL is copied to the test project's + // bin/, so the assembly path doesn't reach the SDK source. Instead, walk + // up from the assembly looking for an `external/openrewrite/rewrite` + // symlink (the convention for source-linked SDK in Conductor / consumer + // repos), then derive the classpath from the symlink target. + var sdkAssembly = typeof(OpenRewrite.CSharp.CSharpParser).Assembly.Location; + if (string.IsNullOrEmpty(sdkAssembly)) + return null; + + var dir = Path.GetDirectoryName(sdkAssembly); + while (dir != null) + { + // Case (a): inside the SDK repo. + if (Path.GetFileName(dir) == "OpenRewrite") + { + var parent = Path.GetDirectoryName(dir); + var grandparent = parent != null ? Path.GetDirectoryName(parent) : null; + if (parent != null && grandparent != null && + Path.GetFileName(parent) == "csharp" && + Path.GetFileName(grandparent) == "rewrite-csharp") + { + return Path.Combine(grandparent, "build", "rpc-test-server-classpath.txt"); + } + } + + // Case (b): consuming repo with `external/openrewrite/rewrite` symlink. + var symlink = Path.Combine(dir, "external", "openrewrite", "rewrite"); + if (Directory.Exists(symlink)) + { + var sdkMarker = Path.Combine(symlink, + "rewrite-csharp", "csharp", "OpenRewrite", "OpenRewrite.csproj"); + if (File.Exists(sdkMarker)) + { + return Path.Combine(symlink, + "rewrite-csharp", "build", "rpc-test-server-classpath.txt"); + } + } + dir = Path.GetDirectoryName(dir); + } + return null; + } + public void Dispose() { RewriteRpcServer.SetCurrent(null); diff --git a/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/AddFrameworkReferenceTests.cs b/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/AddFrameworkReferenceTests.cs new file mode 100644 index 00000000000..0a07b41a082 --- /dev/null +++ b/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/AddFrameworkReferenceTests.cs @@ -0,0 +1,127 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using OpenRewrite.CSharp.Recipes; +using OpenRewrite.Test; + +namespace OpenRewrite.Tests.CSharp.Recipes; + +public class AddFrameworkReferenceTests : RewriteTest +{ + [Fact] + public void AddsWhenTriggerMatches() + { + RewriteRun( + spec => spec.SetRecipe(new AddFrameworkReference + { + FrameworkName = "Microsoft.AspNetCore.App", + TriggerPackageGlob = "Microsoft.AspNetCore.*" + }), + CsProj( + """ + + + net10.0 + + + + + + """, + """ + + + net10.0 + + + + + + + + + """ + ) + ); + } + + [Fact] + public void NoChangeWhenSdkImplicitlyImports() + { + // Microsoft.NET.Sdk.Web already imports Microsoft.AspNetCore.App + RewriteRun( + spec => spec.SetRecipe(new AddFrameworkReference + { + FrameworkName = "Microsoft.AspNetCore.App" + }), + CsProj( + """ + + + net10.0 + + + """ + ) + ); + } + + [Fact] + public void NoChangeWhenAlreadyPresent() + { + RewriteRun( + spec => spec.SetRecipe(new AddFrameworkReference + { + FrameworkName = "Microsoft.AspNetCore.App" + }), + CsProj( + """ + + + net10.0 + + + + + + """ + ) + ); + } + + [Fact] + public void NoChangeWhenTriggerNotMatched() + { + RewriteRun( + spec => spec.SetRecipe(new AddFrameworkReference + { + FrameworkName = "Microsoft.AspNetCore.App", + TriggerPackageGlob = "Microsoft.AspNetCore.*" + }), + CsProj( + """ + + + net10.0 + + + + + + """ + ) + ); + } +} diff --git a/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/RemoveDotNetCliToolReferenceTests.cs b/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/RemoveDotNetCliToolReferenceTests.cs new file mode 100644 index 00000000000..64cae992423 --- /dev/null +++ b/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/RemoveDotNetCliToolReferenceTests.cs @@ -0,0 +1,87 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using OpenRewrite.CSharp.Recipes; +using OpenRewrite.Test; + +namespace OpenRewrite.Tests.CSharp.Recipes; + +public class RemoveDotNetCliToolReferenceTests : RewriteTest +{ + [Fact] + public void RemoveSpecificCliTool() + { + RewriteRun( + spec => spec.SetRecipe(new RemoveDotNetCliToolReference + { + ToolName = "Microsoft.DotNet.Watcher.Tools" + }), + CsProj( + """ + + + net10.0 + + + + + + + """, + """ + + + net10.0 + + + + + + """ + ) + ); + } + + [Fact] + public void RemoveAllCliToolsViaWildcard() + { + RewriteRun( + spec => spec.SetRecipe(new RemoveDotNetCliToolReference + { + ToolName = "*" + }), + CsProj( + """ + + + net10.0 + + + + + + + """, + """ + + + net10.0 + + + """ + ) + ); + } +} diff --git a/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/RemoveMSBuildPropertyTests.cs b/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/RemoveMSBuildPropertyTests.cs new file mode 100644 index 00000000000..c4d7a1a7f78 --- /dev/null +++ b/rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/Recipes/RemoveMSBuildPropertyTests.cs @@ -0,0 +1,94 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using OpenRewrite.CSharp.Recipes; +using OpenRewrite.Test; + +namespace OpenRewrite.Tests.CSharp.Recipes; + +public class RemoveMSBuildPropertyTests : RewriteTest +{ + [Fact] + public void RemovesProperty() + { + RewriteRun( + spec => spec.SetRecipe(new RemoveMSBuildProperty + { + PropertyName = "RuntimeFrameworkVersion" + }), + CsProj( + """ + + + net10.0 + 2.1.1 + + + """, + """ + + + net10.0 + + + """ + ) + ); + } + + [Fact] + public void NoChangeWhenPropertyAbsent() + { + RewriteRun( + spec => spec.SetRecipe(new RemoveMSBuildProperty + { + PropertyName = "RuntimeFrameworkVersion" + }), + CsProj( + """ + + + net10.0 + + + """ + ) + ); + } + + [Fact] + public void OnlyMatchesInsidePropertyGroup() + { + // A child element named identically inside an ItemGroup or elsewhere should not be touched. + RewriteRun( + spec => spec.SetRecipe(new RemoveMSBuildProperty + { + PropertyName = "RuntimeFrameworkVersion" + }), + CsProj( + """ + + + net10.0 + + + + + + """ + ) + ); + } +}