Skip to content

Commit 9d86df1

Browse files
authored
C#: Extract csproj visitor classes as public for reuse in custom recipes (#7324)
Extract private inner visitor classes from csproj manipulation recipes into standalone public classes so they can be used directly in custom recipe edit phases.
1 parent aaa4391 commit 9d86df1

10 files changed

Lines changed: 410 additions & 280 deletions

rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/AddNuGetPackageReference.cs

Lines changed: 1 addition & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -48,95 +48,6 @@ public override ITreeVisitor<ExecutionContext> GetVisitor(DotNetBuildContext acc
4848
{
4949
return Preconditions.Check(
5050
new IsProjectFile(),
51-
new AddNuGetVisitor(PackageName, Version));
52-
}
53-
54-
private class AddNuGetVisitor(string packageName, string? version) : XmlVisitor<ExecutionContext>
55-
{
56-
private bool _alreadyPresent;
57-
private Tag? _lastItemGroup;
58-
59-
public override Xml.Xml VisitDocument(Document document, ExecutionContext ctx)
60-
{
61-
_alreadyPresent = false;
62-
_lastItemGroup = null;
63-
64-
// Check if already present via marker
65-
var marker = document.Markers.FindFirst<MSBuildProject>();
66-
if (marker != null)
67-
{
68-
foreach (var tfm in marker.TargetFrameworks)
69-
{
70-
foreach (var pkgRef in tfm.PackageReferences)
71-
{
72-
if (packageName == pkgRef.Include)
73-
{
74-
_alreadyPresent = true;
75-
break;
76-
}
77-
}
78-
if (_alreadyPresent) break;
79-
}
80-
}
81-
82-
if (_alreadyPresent)
83-
return document;
84-
85-
var d = (Document)base.VisitDocument(document, ctx);
86-
87-
if (_alreadyPresent)
88-
return d;
89-
90-
// Build the new PackageReference tag
91-
var tag = version != null
92-
? $"<PackageReference Include=\"{packageName}\" Version=\"{version}\" />"
93-
: $"<PackageReference Include=\"{packageName}\" />";
94-
var newRef = TagExtensions.BuildTag(tag);
95-
96-
if (_lastItemGroup != null)
97-
{
98-
DoAfterVisit(new AddToTagVisitor<ExecutionContext>(_lastItemGroup, newRef));
99-
}
100-
else
101-
{
102-
var itemGroup = TagExtensions.BuildTag(
103-
$"<ItemGroup>\n {tag}\n </ItemGroup>");
104-
DoAfterVisit(new AddToTagVisitor<ExecutionContext>(d.Root, itemGroup));
105-
}
106-
107-
DoAfterVisit(MSBuildProjectHelper.RegenerateMarkerVisitor());
108-
return d;
109-
}
110-
111-
public override Xml.Xml VisitTag(Tag tag, ExecutionContext ctx)
112-
{
113-
var t = (Tag)base.VisitTag(tag, ctx);
114-
115-
// Check XML directly for idempotency
116-
if (t.Name == "PackageReference")
117-
{
118-
var include = t.GetAttributeValue("Include");
119-
if (packageName == include)
120-
_alreadyPresent = true;
121-
}
122-
123-
// Track the last ItemGroup that contains PackageReference elements
124-
if (t.Name == "ItemGroup")
125-
{
126-
var hasPackageRef = false;
127-
foreach (var child in t.GetChildren())
128-
{
129-
if (child.Name == "PackageReference")
130-
{
131-
hasPackageRef = true;
132-
break;
133-
}
134-
}
135-
if (hasPackageRef)
136-
_lastItemGroup = t;
137-
}
138-
139-
return t;
140-
}
51+
new AddNuGetPackageReferenceVisitor(PackageName, Version));
14152
}
14253
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
using OpenRewrite.Core;
17+
using OpenRewrite.Xml;
18+
using ExecutionContext = OpenRewrite.Core.ExecutionContext;
19+
20+
namespace OpenRewrite.CSharp.Recipes;
21+
22+
/// <summary>
23+
/// Visitor that adds a NuGet PackageReference to .csproj files if not already present.
24+
/// Can be used standalone in custom recipe edit phases.
25+
/// </summary>
26+
public class AddNuGetPackageReferenceVisitor(string packageName, string? version) : XmlVisitor<ExecutionContext>
27+
{
28+
private bool _alreadyPresent;
29+
private Tag? _lastItemGroup;
30+
31+
public override Xml.Xml VisitDocument(Document document, ExecutionContext ctx)
32+
{
33+
_alreadyPresent = false;
34+
_lastItemGroup = null;
35+
36+
// Check if already present via marker
37+
var marker = document.Markers.FindFirst<MSBuildProject>();
38+
if (marker != null)
39+
{
40+
foreach (var tfm in marker.TargetFrameworks)
41+
{
42+
foreach (var pkgRef in tfm.PackageReferences)
43+
{
44+
if (packageName == pkgRef.Include)
45+
{
46+
_alreadyPresent = true;
47+
break;
48+
}
49+
}
50+
if (_alreadyPresent) break;
51+
}
52+
}
53+
54+
if (_alreadyPresent)
55+
return document;
56+
57+
var d = (Document)base.VisitDocument(document, ctx);
58+
59+
if (_alreadyPresent)
60+
return d;
61+
62+
// Build the new PackageReference tag
63+
var tag = version != null
64+
? $"<PackageReference Include=\"{packageName}\" Version=\"{version}\" />"
65+
: $"<PackageReference Include=\"{packageName}\" />";
66+
var newRef = TagExtensions.BuildTag(tag);
67+
68+
if (_lastItemGroup != null)
69+
{
70+
DoAfterVisit(new AddToTagVisitor<ExecutionContext>(_lastItemGroup, newRef));
71+
}
72+
else
73+
{
74+
var itemGroup = TagExtensions.BuildTag(
75+
$"<ItemGroup>\n {tag}\n </ItemGroup>");
76+
DoAfterVisit(new AddToTagVisitor<ExecutionContext>(d.Root, itemGroup));
77+
}
78+
79+
DoAfterVisit(MSBuildProjectHelper.RegenerateMarkerVisitor());
80+
return d;
81+
}
82+
83+
public override Xml.Xml VisitTag(Tag tag, ExecutionContext ctx)
84+
{
85+
var t = (Tag)base.VisitTag(tag, ctx);
86+
87+
// Check XML directly for idempotency
88+
if (t.Name == "PackageReference")
89+
{
90+
var include = t.GetAttributeValue("Include");
91+
if (packageName == include)
92+
_alreadyPresent = true;
93+
}
94+
95+
// Track the last ItemGroup that contains PackageReference elements
96+
if (t.Name == "ItemGroup")
97+
{
98+
var hasPackageRef = false;
99+
foreach (var child in t.GetChildren())
100+
{
101+
if (child.Name == "PackageReference")
102+
{
103+
hasPackageRef = true;
104+
break;
105+
}
106+
}
107+
if (hasPackageRef)
108+
_lastItemGroup = t;
109+
}
110+
111+
return t;
112+
}
113+
}

rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/ChangeDotNetTargetFramework.cs

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -49,62 +49,6 @@ public override ITreeVisitor<ExecutionContext> GetVisitor(DotNetBuildContext acc
4949
{
5050
return Preconditions.Check(
5151
new IsProjectFile(),
52-
new ChangeTfmVisitor(OldTargetFramework, NewTargetFramework));
53-
}
54-
55-
private class ChangeTfmVisitor(string oldTfm, string newTfm) : XmlVisitor<ExecutionContext>
56-
{
57-
private bool _modified;
58-
59-
public override Xml.Xml VisitDocument(Document document, ExecutionContext ctx)
60-
{
61-
_modified = false;
62-
var d = (Document)base.VisitDocument(document, ctx);
63-
if (_modified)
64-
DoAfterVisit(MSBuildProjectHelper.RegenerateMarkerVisitor());
65-
return d;
66-
}
67-
68-
public override Xml.Xml VisitTag(Tag tag, ExecutionContext ctx)
69-
{
70-
var t = (Tag)base.VisitTag(tag, ctx);
71-
72-
if (t.Name == "TargetFramework")
73-
{
74-
var value = t.GetValue() ?? "";
75-
if (oldTfm == value)
76-
{
77-
_modified = true;
78-
DoAfterVisit(new ChangeTagValueVisitor<ExecutionContext>(t, newTfm));
79-
}
80-
}
81-
else if (t.Name == "TargetFrameworks")
82-
{
83-
var value = t.GetValue() ?? "";
84-
var frameworks = value.Split(';');
85-
var changed = false;
86-
var seen = new LinkedList<string>();
87-
foreach (var framework in frameworks)
88-
{
89-
var fw = framework.Trim();
90-
if (oldTfm == fw)
91-
{
92-
changed = true;
93-
fw = newTfm;
94-
}
95-
if (!seen.Contains(fw))
96-
seen.AddLast(fw);
97-
}
98-
99-
if (changed)
100-
{
101-
_modified = true;
102-
DoAfterVisit(new ChangeTagValueVisitor<ExecutionContext>(
103-
t, string.Join(";", seen)));
104-
}
105-
}
106-
107-
return t;
108-
}
52+
new ChangeDotNetTargetFrameworkVisitor(OldTargetFramework, NewTargetFramework));
10953
}
11054
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
using OpenRewrite.Core;
17+
using OpenRewrite.Xml;
18+
using ExecutionContext = OpenRewrite.Core.ExecutionContext;
19+
20+
namespace OpenRewrite.CSharp.Recipes;
21+
22+
/// <summary>
23+
/// Visitor that changes the target framework in .csproj files.
24+
/// Handles both single-TFM (&lt;TargetFramework&gt;) and multi-TFM (&lt;TargetFrameworks&gt;) elements.
25+
/// Can be used standalone in custom recipe edit phases.
26+
/// </summary>
27+
public class ChangeDotNetTargetFrameworkVisitor(string oldTfm, string newTfm) : XmlVisitor<ExecutionContext>
28+
{
29+
private bool _modified;
30+
31+
public override Xml.Xml VisitDocument(Document document, ExecutionContext ctx)
32+
{
33+
_modified = false;
34+
var d = (Document)base.VisitDocument(document, ctx);
35+
if (_modified)
36+
DoAfterVisit(MSBuildProjectHelper.RegenerateMarkerVisitor());
37+
return d;
38+
}
39+
40+
public override Xml.Xml VisitTag(Tag tag, ExecutionContext ctx)
41+
{
42+
var t = (Tag)base.VisitTag(tag, ctx);
43+
44+
if (t.Name == "TargetFramework")
45+
{
46+
var value = t.GetValue() ?? "";
47+
if (oldTfm == value)
48+
{
49+
_modified = true;
50+
DoAfterVisit(new ChangeTagValueVisitor<ExecutionContext>(t, newTfm));
51+
}
52+
}
53+
else if (t.Name == "TargetFrameworks")
54+
{
55+
var value = t.GetValue() ?? "";
56+
var frameworks = value.Split(';');
57+
var changed = false;
58+
var seen = new LinkedList<string>();
59+
foreach (var framework in frameworks)
60+
{
61+
var fw = framework.Trim();
62+
if (oldTfm == fw)
63+
{
64+
changed = true;
65+
fw = newTfm;
66+
}
67+
if (!seen.Contains(fw))
68+
seen.AddLast(fw);
69+
}
70+
71+
if (changed)
72+
{
73+
_modified = true;
74+
DoAfterVisit(new ChangeTagValueVisitor<ExecutionContext>(
75+
t, string.Join(";", seen)));
76+
}
77+
}
78+
79+
return t;
80+
}
81+
}

rewrite-csharp/csharp/OpenRewrite/CSharp/Recipes/FindNuGetPackageReference.cs

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,25 +42,4 @@ public override ITreeVisitor<ExecutionContext> GetVisitor()
4242
new IsProjectFile(),
4343
new FindNuGetPackageReferenceVisitor(PackageName));
4444
}
45-
46-
private class FindNuGetPackageReferenceVisitor(string packageName) : XmlVisitor<ExecutionContext>
47-
{
48-
public override Xml.Xml VisitDocument(Document document, ExecutionContext ctx)
49-
{
50-
var marker = document.Markers.FindFirst<MSBuildProject>();
51-
if (marker != null)
52-
{
53-
foreach (var tfm in marker.TargetFrameworks)
54-
{
55-
foreach (var pkgRef in tfm.PackageReferences)
56-
{
57-
if (GlobMatcher.Matches(pkgRef.Include, packageName))
58-
return document.WithMarkers(
59-
document.Markers.Add(new SearchResult(Guid.NewGuid(), null)));
60-
}
61-
}
62-
}
63-
return document;
64-
}
65-
}
6645
}

0 commit comments

Comments
 (0)