Skip to content

Commit 445a08b

Browse files
paulirwinclaude
andauthored
Add analyzer and code fix for public types in Lucene.Net.Support, #1100 (#2)
* Add analyzer for public types in Support namespace * Remove migrated resource for code fix title * Remove .globalconfig file * Upgrade RAT to 0.16.1, clean up line endings after running * Fix casing of code fix provider name * Fix code fix with partial classes * Revert Markdown changes and exclude from RAT * RAT exclude only AnalyzerReleases files; fix warning about AnalyzerReleases version number * Use MajorMinorVersion instead of Version * Fix code fix to preserve license headers and comments The code fix was stripping leading trivia (license headers, comments) when changing public to internal. The fix now preserves both leading trivia from the first modifier and trailing trivia from the accessibility modifier being replaced. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent df8e076 commit 445a08b

22 files changed

+1275
-24
lines changed

.rat-excludes

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,7 @@ _site/*
2020

2121
release-build-outcomes\.svg
2222
release-workflow\.svg
23+
24+
# Exclude analyzer releases markdown files
25+
AnalyzerReleases\..*\.md
26+

docs/images/release-build-outcomes.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
120
This markup can be edited and converted to .svg or .png here:
221
https://www.mermaidchart.com/app/projects/95759d78-db93-499c-ad66-0e3f698ba88c/diagrams/31dbd6bc-7ec8-4583-a456-55e3fe3f6cfc/version/v0.1/edit
322

docs/images/release-workflow.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
120
This markup can be edited and converted to .svg or .png here:
221
https://www.mermaidchart.com/app/projects/95759d78-db93-499c-ad66-0e3f698ba88c/diagrams/35faa26e-5ccf-4433-962e-32f20496471c/version/v0.1/edit
322

docs/make-release.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,12 @@ Since Nerdbank.GitVersioning calculates the release version, the `AnalyzerReleas
282282

283283
`AnalyzerReleases.Shipped.md` evolves by appending each release as a new section. Each release is marked with a `## Release <version>` header.
284284

285+
> [!NOTE]
286+
> Due to a limitation/bug in the Roslyn meta-analyzers, the Release header cannot contain semver pre-release labels (e.g., `-beta`, `-rc`, etc.).
287+
> Therefore, the version token `{{vnext}}` is used to indicate the next minor release version, which will be replaced with the actual version during the git post-commit hook.
288+
285289
```markdown
286-
## Release 2.0.0-alpha.1
290+
## Release 2.0
287291

288292
### New Rules
289293

eng/git-hooks/post-commit

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,19 @@ The 'nbgv' tool is required but not installed.
4545
To recover manually:
4646
4747
1. Install the nbgv tool (see the docs/make-release.md documentation)
48-
2. Run: nbgv get-version -v NuGetPackageVersion
48+
2. Run: nbgv get-version -v MajorMinorVersion
4949
3. In $file, replace the $token token with the version returned from step 2
5050
4. Run: git add $file && git commit --amend --no-edit
51-
5. Run: nbgv get-version -v NuGetPackageVersion again to ensure the version is the same as step 2 before proceeding
51+
5. Run: nbgv get-version -v MajorMinorVersion again to ensure the version is the same as step 2 before proceeding
5252
EOF
5353
exit 1
5454
fi
5555

5656
# Set flag to prevent recursion
5757
export POST_COMMIT_RUNNING=1
5858

59-
# Get the NuGet version
60-
version=$(nbgv get-version -v NuGetPackageVersion)
59+
# Get the NuGet version without any prerelease suffix, which is not compatible with the Roslyn meta-analyzers
60+
version=$(nbgv get-version -v MajorMinorVersion)
6161
echo "Replacing '$token' with '$version' in '$file'"
6262

6363
# Replace {{vnext}} only in lines starting with "## Release "

rat.ps1

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
directory as this script) as the target directory.
3131
3232
.PARAMETER Version
33-
The version of Apache RAT to use (default: 0.13).
33+
The version of Apache RAT to use (default: 0.16.1).
3434
3535
.PARAMETER ExcludeFileName
3636
Name of an exclude file containing path patterns that RAT should ignore.
@@ -43,19 +43,19 @@
4343
.EXAMPLE
4444
pwsh ./rat.ps1
4545
46-
Runs Apache RAT (default version 0.13) with exclusions from `rat-exclude.txt`.
46+
Runs Apache RAT (default version 0.16.1) with exclusions from `rat-exclude.txt`.
4747
4848
.EXAMPLE
49-
pwsh ./rat.ps1 -Version 0.13 -ExcludeFileName custom-exclude.txt
49+
pwsh ./rat.ps1 -Version 0.16.1 -ExcludeFileName custom-exclude.txt
5050
51-
Runs Apache RAT version 0.13 using the specified exclude file.
51+
Runs Apache RAT version 0.16.1 using the specified exclude file.
5252
5353
.NOTES
5454
This script is intended for use by release managers when preparing official
5555
ASF releases. It is not normally required for day-to-day development.
5656
#>
5757
param(
58-
[string]$Version = "0.13",
58+
[string]$Version = "0.16.1",
5959
[string]$ExcludeFileName = ".rat-excludes"
6060
)
6161

@@ -90,13 +90,13 @@ if (-not (Test-Path $ratExcludeFile)) {
9090

9191
$argsList = @(
9292
"-jar", $ratJar,
93-
"--dir", "`"$scriptDir`"",
93+
"--dir", "$scriptDir",
9494
"--addLicense",
9595
"--force"
9696
)
9797

9898
if ($useExclude) {
99-
$argsList += @("--exclude-file", "`"$ratExcludeFile`"")
99+
$argsList += @("--exclude-file", "$ratExcludeFile")
100100
}
101101

102102
# Call java with argument list. Use & to invoke program.
@@ -105,3 +105,31 @@ if ($useExclude) {
105105
if ($LASTEXITCODE -ne 0) {
106106
throw "RAT exited with code $LASTEXITCODE"
107107
}
108+
109+
# Remove trailing whitespace from files modified by RAT
110+
Write-Host "Removing trailing whitespace from modified files..."
111+
112+
# Get list of modified files from git
113+
$modifiedFiles = git diff --name-only --diff-filter=M
114+
if ($LASTEXITCODE -ne 0) {
115+
Write-Host "Warning: Could not get modified files list from git."
116+
} else {
117+
foreach ($file in $modifiedFiles) {
118+
if ([string]::IsNullOrWhiteSpace($file)) { continue }
119+
120+
$filePath = Join-Path $scriptDir $file
121+
if (-not (Test-Path $filePath)) { continue }
122+
123+
try {
124+
# Read all lines, trim trailing whitespace, and write back
125+
$lines = Get-Content -Path $filePath
126+
if ($null -ne $lines) {
127+
$trimmedLines = $lines | ForEach-Object { $_.TrimEnd() }
128+
$trimmedLines | Set-Content -Path $filePath -NoNewline:$false
129+
Write-Host " Cleaned: $file"
130+
}
131+
} catch {
132+
Write-Host " Warning: Could not process $file : $_"
133+
}
134+
}
135+
}

src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/CodeFixResources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,7 @@ under the License.
122122
<value>Use {0}</value>
123123
<comment>Title for code fix; {0} is the code element to utilize.</comment>
124124
</data>
125+
<data name="MakeXInternal" xml:space="preserve">
126+
<value>Make {0} internal</value>
127+
</data>
125128
</root>
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
using System.Collections.Immutable;
19+
using System.Composition;
20+
using System.Linq;
21+
using System.Threading;
22+
using System.Threading.Tasks;
23+
using Lucene.Net.CodeAnalysis.Dev.CodeFixes;
24+
using Lucene.Net.CodeAnalysis.Dev.CodeFixes.Utility;
25+
using Lucene.Net.CodeAnalysis.Dev.Utility;
26+
using Microsoft.CodeAnalysis;
27+
using Microsoft.CodeAnalysis.CodeFixes;
28+
using Microsoft.CodeAnalysis.CSharp.Syntax;
29+
using SyntaxFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
30+
using SyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind;
31+
32+
namespace Lucene.Net.CodeAnalysis.Dev;
33+
34+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider)), Shared]
35+
public class LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider : CodeFixProvider
36+
{
37+
// Specify the diagnostic IDs of analyzers that are expected to be linked.
38+
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
39+
[Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.Id];
40+
41+
public override FixAllProvider? GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
42+
43+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
44+
{
45+
// We link only one diagnostic and assume there is only one diagnostic in the context.
46+
var diagnostic = context.Diagnostics.Single();
47+
48+
// 'SourceSpan' of 'Location' is the highlighted area. We're going to use this area to find the 'SyntaxNode' to rename.
49+
var diagnosticSpan = diagnostic.Location.SourceSpan;
50+
51+
// Get the root of Syntax Tree that contains the highlighted diagnostic.
52+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
53+
54+
// Find SyntaxNode corresponding to the diagnostic.
55+
var diagnosticNode = root?.FindNode(diagnosticSpan);
56+
57+
if (diagnosticNode is MemberDeclarationSyntax declaration)
58+
{
59+
var name = declaration switch
60+
{
61+
BaseTypeDeclarationSyntax baseTypeDeclaration => baseTypeDeclaration.Identifier.ToString(),
62+
DelegateDeclarationSyntax delegateDeclaration => delegateDeclaration.Identifier.ToString(),
63+
_ => null
64+
};
65+
66+
if (name == null)
67+
{
68+
return;
69+
}
70+
71+
// Register a code action that will invoke the fix.
72+
context.RegisterCodeFix(
73+
CodeActionHelper.CreateFromResource(
74+
CodeFixResources.MakeXInternal,
75+
createChangedSolution: c => MakeDeclarationInternal(context.Document, declaration, c),
76+
"MakeDeclarationInternal",
77+
name),
78+
diagnostic);
79+
}
80+
}
81+
82+
private static async Task<Solution> MakeDeclarationInternal(Document document,
83+
MemberDeclarationSyntax memberDeclaration,
84+
CancellationToken cancellationToken)
85+
{
86+
var solution = document.Project.Solution;
87+
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
88+
if (semanticModel == null) return solution;
89+
90+
// Get the symbol for this type declaration
91+
var symbol = semanticModel.GetDeclaredSymbol(memberDeclaration, cancellationToken);
92+
if (symbol == null) return solution;
93+
94+
// Find all partial declarations of this symbol
95+
var declaringSyntaxReferences = symbol.DeclaringSyntaxReferences;
96+
97+
// Update all partial declarations across all documents
98+
foreach (var syntaxReference in declaringSyntaxReferences)
99+
{
100+
var declarationSyntax = await syntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false);
101+
if (declarationSyntax is not MemberDeclarationSyntax declaration) continue;
102+
103+
var declarationDocument = solution.GetDocument(syntaxReference.SyntaxTree);
104+
if (declarationDocument == null) continue;
105+
106+
var syntaxRoot = await declarationDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
107+
if (syntaxRoot == null) continue;
108+
109+
// Get leading trivia from the first modifier (which contains license headers/comments)
110+
var leadingTrivia = declaration.Modifiers.Count > 0
111+
? declaration.Modifiers[0].LeadingTrivia
112+
: SyntaxTriviaList.Empty;
113+
114+
// Get trailing trivia from the accessibility modifier we're removing (typically whitespace)
115+
var accessibilityModifier = declaration.Modifiers
116+
.FirstOrDefault(m => m.IsKind(SyntaxKind.PublicKeyword) ||
117+
m.IsKind(SyntaxKind.InternalKeyword) ||
118+
m.IsKind(SyntaxKind.ProtectedKeyword) ||
119+
m.IsKind(SyntaxKind.PrivateKeyword));
120+
var trailingTrivia = accessibilityModifier.TrailingTrivia;
121+
122+
// Remove existing accessibility modifiers
123+
var newModifiers = SyntaxFactory.TokenList(
124+
declaration.Modifiers
125+
.Where(modifier => !modifier.IsKind(SyntaxKind.PrivateKeyword) &&
126+
!modifier.IsKind(SyntaxKind.ProtectedKeyword) &&
127+
!modifier.IsKind(SyntaxKind.InternalKeyword) &&
128+
!modifier.IsKind(SyntaxKind.PublicKeyword))
129+
).Insert(0, SyntaxFactory.Token(leadingTrivia, SyntaxKind.InternalKeyword, trailingTrivia)); // Ensure 'internal' is the first modifier with preserved trivia
130+
131+
var newDeclaration = declaration.WithModifiers(newModifiers);
132+
var newRoot = syntaxRoot.ReplaceNode(declaration, newDeclaration);
133+
solution = solution.WithDocumentSyntaxRoot(declarationDocument.Id, newRoot);
134+
}
135+
136+
return solution;
137+
}
138+
}

src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/Utility/CodeActionHelper.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,18 @@ public static CodeAction CreateFromResource(
3737
var title = string.Format(resourceValue, args);
3838
return CodeAction.Create(title, createChangedDocument, equivalenceKey);
3939
}
40+
41+
/// <summary>
42+
/// Create a CodeAction using a resource string and formatting arguments.
43+
/// </summary>
44+
public static CodeAction CreateFromResource(
45+
string resourceValue,
46+
Func<CancellationToken, Task<Solution>> createChangedSolution,
47+
string equivalenceKey,
48+
params object[] args)
49+
{
50+
var title = string.Format(resourceValue, args);
51+
return CodeAction.Create(title, createChangedSolution, equivalenceKey);
52+
}
4053
}
4154
}

src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ under the License.
3232
<_PackageVersionPropsFilePath>$(_NuGetPackageOutputPath)\Lucene.Net.CodeAnalysis.Dev.Version.props</_PackageVersionPropsFilePath>
3333
<!-- We install the analyzer package in a local directory so we don't pollute the
3434
.nuget cache on the dev machine with temporary builds -->
35-
<RestorePackagesPath>obj\LocalNuGetPackages</RestorePackagesPath>
36-
<_RestorePackagesPath>$(RestorePackagesPath)\lucene.net.codeanalsis.dev</_RestorePackagesPath>
35+
<RestorePackagesPath>obj/LocalNuGetPackages</RestorePackagesPath>
36+
<_RestorePackagesPath>$(RestorePackagesPath)/lucene.net.codeanalysis.dev</_RestorePackagesPath>
3737
</PropertyGroup>
3838

3939
<PropertyGroup Condition="Exists('$(_NuGetPackageOutputPath)')">

0 commit comments

Comments
 (0)