From fa11b3015347c9bf9729825fa7111d796b14adad Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Sat, 8 Mar 2025 15:53:58 -0700 Subject: [PATCH 01/10] Add analyzer for public types in Support namespace --- .globalconfig | 2 + .../CodeFixResources.resx | 3 + ...eNetSupportPublicTypesCsCodeFixProvider.cs | 102 ++++++ .../Utility/CodeActionHelper.cs | 13 + .../Lucene.Net.CodeAnalysis.Dev.Sample.csproj | 4 +- ...uceneDev1005Sample_BlockScopedNamespace.cs | 28 ++ ...LuceneDev1005Sample_FileScopedNamespace.cs | 27 ++ .../AnalyzerReleases.Unshipped.md | 3 +- ...ceneNetSupportPublicTypesCSCodeAnalyzer.cs | 81 +++++ .../Resources.resx | 12 + .../Utility/Category.cs | 8 +- .../Utility/Descriptors.LuceneDev1xxx.cs | 11 +- ...eNetSupportPublicTypesCSCodeFixProvider.cs | 107 ++++++ ...ceneNetSupportPublicTypesCSCodeAnalyzer.cs | 336 ++++++++++++++++++ 14 files changed, 726 insertions(+), 11 deletions(-) create mode 100644 .globalconfig create mode 100644 src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_BlockScopedNamespace.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_FileScopedNamespace.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer.cs create mode 100644 tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs create mode 100644 tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer.cs diff --git a/.globalconfig b/.globalconfig new file mode 100644 index 0000000..de8b7b1 --- /dev/null +++ b/.globalconfig @@ -0,0 +1,2 @@ +roslyn_correctness.assembly_reference_validation = relaxed + diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/CodeFixResources.resx b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/CodeFixResources.resx index d4db943..0c5634c 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/CodeFixResources.resx +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/CodeFixResources.resx @@ -122,4 +122,7 @@ under the License. Use {0} Title for code fix; {0} is the code element to utilize. + + Make {0} internal + diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider.cs new file mode 100644 index 0000000..feefe0e --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider.cs @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Lucene.Net.CodeAnalysis.Dev.CodeFixes; +using Lucene.Net.CodeAnalysis.Dev.CodeFixes.Utility; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SyntaxFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using SyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind; + +namespace Lucene.Net.CodeAnalysis.Dev; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider)), Shared] +public class LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider : CodeFixProvider +{ + // Specify the diagnostic IDs of analyzers that are expected to be linked. + public sealed override ImmutableArray FixableDiagnosticIds { get; } = + [Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.Id]; + + public override FixAllProvider? GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + // We link only one diagnostic and assume there is only one diagnostic in the context. + var diagnostic = context.Diagnostics.Single(); + + // 'SourceSpan' of 'Location' is the highlighted area. We're going to use this area to find the 'SyntaxNode' to rename. + var diagnosticSpan = diagnostic.Location.SourceSpan; + + // Get the root of Syntax Tree that contains the highlighted diagnostic. + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // Find SyntaxNode corresponding to the diagnostic. + var diagnosticNode = root?.FindNode(diagnosticSpan); + + if (diagnosticNode is MemberDeclarationSyntax declaration) + { + var name = declaration switch + { + BaseTypeDeclarationSyntax baseTypeDeclaration => baseTypeDeclaration.Identifier.ToString(), + DelegateDeclarationSyntax delegateDeclaration => delegateDeclaration.Identifier.ToString(), + _ => null + }; + + if (name == null) + { + return; + } + + // Register a code action that will invoke the fix. + context.RegisterCodeFix( + CodeActionHelper.CreateFromResource( + CodeFixResources.MakeXInternal, + createChangedSolution: c => MakeDeclarationInternal(context.Document, declaration, c), + "MakeDeclarationInternal", + name), + diagnostic); + } + } + + private async Task MakeDeclarationInternal(Document document, + MemberDeclarationSyntax memberDeclaration, + CancellationToken cancellationToken) + { + var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (syntaxRoot == null) return document.Project.Solution; + + // Remove existing accessibility modifiers + var newModifiers = SyntaxFactory.TokenList( + memberDeclaration.Modifiers + .Where(modifier => !modifier.IsKind(SyntaxKind.PrivateKeyword) && + !modifier.IsKind(SyntaxKind.ProtectedKeyword) && + !modifier.IsKind(SyntaxKind.InternalKeyword) && + !modifier.IsKind(SyntaxKind.PublicKeyword)) + ).Insert(0, SyntaxFactory.Token(SyntaxKind.InternalKeyword)); // Ensure 'internal' is the first modifier + + var newMemberDeclaration = memberDeclaration.WithModifiers(newModifiers); + var newRoot = syntaxRoot.ReplaceNode(memberDeclaration, newMemberDeclaration); + return document.Project.Solution.WithDocumentSyntaxRoot(document.Id, newRoot); + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/Utility/CodeActionHelper.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/Utility/CodeActionHelper.cs index e9a68c2..0892d68 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/Utility/CodeActionHelper.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/Utility/CodeActionHelper.cs @@ -37,5 +37,18 @@ public static CodeAction CreateFromResource( var title = string.Format(resourceValue, args); return CodeAction.Create(title, createChangedDocument, equivalenceKey); } + + /// + /// Create a CodeAction using a resource string and formatting arguments. + /// + public static CodeAction CreateFromResource( + string resourceValue, + Func> createChangedSolution, + string equivalenceKey, + params object[] args) + { + var title = string.Format(resourceValue, args); + return CodeAction.Create(title, createChangedSolution, equivalenceKey); + } } } diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj b/src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj index 7ac3045..baa0e34 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj @@ -32,8 +32,8 @@ under the License. <_PackageVersionPropsFilePath>$(_NuGetPackageOutputPath)\Lucene.Net.CodeAnalysis.Dev.Version.props - obj\LocalNuGetPackages - <_RestorePackagesPath>$(RestorePackagesPath)\lucene.net.codeanalsis.dev + obj/LocalNuGetPackages + <_RestorePackagesPath>$(RestorePackagesPath)/lucene.net.codeanalysis.dev diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_BlockScopedNamespace.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_BlockScopedNamespace.cs new file mode 100644 index 0000000..03014da --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_BlockScopedNamespace.cs @@ -0,0 +1,28 @@ +namespace Lucene.Net.Support.BlockScoped +{ + public class PublicClass + { + } + + public interface PublicInterface + { + } + + public enum PublicEnum + { + } + + public delegate void PublicDelegate(); + + public struct PublicStruct + { + } + + public record PublicRecord + { + } + + public record struct PublicRecordStruct + { + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_FileScopedNamespace.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_FileScopedNamespace.cs new file mode 100644 index 0000000..2797961 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_FileScopedNamespace.cs @@ -0,0 +1,27 @@ +namespace Lucene.Net.Support; + +public class PublicClass +{ +} + +public interface PublicInterface +{ +} + +public enum PublicEnum +{ +} + +public delegate void PublicDelegate(); + +public struct PublicStruct +{ +} + +public record PublicRecord +{ +} + +public record struct PublicRecordStruct +{ +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md index cb12062..f8f5973 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md +++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md @@ -1,4 +1,5 @@ ### New Rules - Rule ID | Category | Severity | Notes + Rule ID | Category | Severity | Notes ---------------|----------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------- + LuceneDev1005 | Design | Warning | Types in the Lucene.Net.Support namespace should not be public diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer.cs new file mode 100644 index 0000000..94f0ef6 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer.cs @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using Lucene.Net.CodeAnalysis.Dev.Utility; + +namespace Lucene.Net.CodeAnalysis.Dev +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => [Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntax, + SyntaxKind.ClassDeclaration, + SyntaxKind.EnumDeclaration, + SyntaxKind.InterfaceDeclaration, + SyntaxKind.RecordDeclaration, + SyntaxKind.StructDeclaration, + SyntaxKind.RecordStructDeclaration, + SyntaxKind.DelegateDeclaration); + } + + private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + // any public types in the Lucene.Net.Support (or child) namespace should raise the diagnostic + if (context.Node.Parent is not BaseNamespaceDeclarationSyntax namespaceDeclarationSyntax + || !namespaceDeclarationSyntax.Name.ToString().StartsWith("Lucene.Net.Support")) + { + return; + } + + if (context.Node is DelegateDeclarationSyntax delegateDeclarationSyntax + && delegateDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword)) + { + const string typeKind = "Delegate"; + context.ReportDiagnostic(Diagnostic.Create(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes, delegateDeclarationSyntax.GetLocation(), typeKind, delegateDeclarationSyntax.Identifier.ToString())); + } + else if (context.Node is BaseTypeDeclarationSyntax baseTypeDeclarationSyntax + && baseTypeDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword)) + { + var typeKind = context.Node switch + { + ClassDeclarationSyntax => "Class", + EnumDeclarationSyntax => "Enum", + InterfaceDeclarationSyntax => "Interface", + RecordDeclarationSyntax record when record.ClassOrStructKeyword.IsKind(SyntaxKind.StructKeyword) => "Record struct", + RecordDeclarationSyntax => "Record", + StructDeclarationSyntax => "Struct", + _ => "Type", // should not happen + }; + + context.ReportDiagnostic(Diagnostic.Create(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes, baseTypeDeclarationSyntax.GetLocation(), typeKind, baseTypeDeclarationSyntax.Identifier.ToString())); + } + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx index d61ab13..4f6ecc3 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx +++ b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx @@ -197,4 +197,16 @@ under the License. Methods that return array types should be analyzed to determine whether they are better suited to be one or more out parameters or to return a ValueTuple The title of the diagnostic. + + Types in the Support namespace should not be public + + + Types in the Lucene.Net.Support namespace should not be public. + + + {0} '{1}' should not have public accessibility in the Support namespace + + + Make {0} internal + diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Category.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Category.cs index 21b6be6..e421adc 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Category.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Category.cs @@ -6,9 +6,9 @@ * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * 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 @@ -17,10 +17,6 @@ * under the License. */ -using System; -using System.Collections.Generic; -using System.Text; - namespace Lucene.Net.CodeAnalysis.Dev.Utility { public enum Category diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev1xxx.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev1xxx.cs index 471a667..fcf5cb5 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev1xxx.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev1xxx.cs @@ -6,9 +6,9 @@ * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * 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 @@ -63,5 +63,12 @@ public static partial class Descriptors Design, Warning ); + + public static readonly DiagnosticDescriptor LuceneDev1005_LuceneNetSupportPublicTypes = + Diagnostic( + "LuceneDev1005", + Design, + Warning + ); } } diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs new file mode 100644 index 0000000..221ebad --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes; + +public class TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider +{ + [Test] + public async Task PublicTypeInSupport_FileScopedNamespace_MakeInternalFix() + { + const string testCode = + """ + namespace Lucene.Net.Support; + + public class MyClass + { + } + """; + + const string fixedCode = + """ + namespace Lucene.Net.Support; + + internal class MyClass + { + } + """; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments("Class", "MyClass") + .WithLocation(3, 1); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer(), + () => new LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider()) + { + TestCode = testCode.ReplaceLineEndings(), + FixedCode = fixedCode.ReplaceLineEndings(), + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task PublicTypeInSupport_BlockScopedNamespace_MakeInternalFix() + { + const string testCode = + """ + namespace Lucene.Net.Support + { + public class MyClass + { + } + } + """; + + const string fixedCode = + """ + namespace Lucene.Net.Support + { + internal class MyClass + { + } + } + """; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments("Class", "MyClass") + .WithLocation(3, 5); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer(), + () => new LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider()) + { + TestCode = testCode.ReplaceLineEndings(), + FixedCode = fixedCode.ReplaceLineEndings(), + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer.cs new file mode 100644 index 0000000..6d1c9b6 --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer.cs @@ -0,0 +1,336 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 System.Threading.Tasks; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; + +namespace Lucene.Net.CodeAnalysis.Dev +{ + public class TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer + { + //No diagnostics expected to show up + [Test] + public async Task TestEmptyFile() + { + const string testCode = ""; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + [TestCase("class")] + [TestCase("struct")] + [TestCase("interface")] + [TestCase("record")] + [TestCase("record struct")] + [TestCase("enum")] + public async Task TestDiagnostic_FileScopedNamespace_PositiveTest(string typeKind) + { + string typeKindDesc = typeKind[0].ToString().ToUpper() + typeKind[1..]; + string typeName = $"Public{typeKindDesc.Replace(" ", "")}"; + + string testCode = + $""" + namespace Lucene.Net.Support; + public {typeKind} {typeName}; + """; + + var expected = DiagnosticResult.CompilerWarning(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.Id) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments(typeKindDesc, typeName) + .WithLocation(2, 1); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + [TestCase("class")] + [TestCase("struct")] + [TestCase("interface")] + [TestCase("record")] + [TestCase("record struct")] + [TestCase("enum")] + public async Task TestDiagnostic_FileScopedNamespace_NegativeTest_PublicInAnotherNamespace(string typeKind) + { + string typeKindDesc = typeKind[0].ToString().ToUpper() + typeKind[1..]; + string typeName = $"Public{typeKindDesc.Replace(" ", "")}"; + + string testCode = + $""" + namespace Lucene.Net.SomethingElse; + public {typeKind} {typeName}; + """; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + [TestCase("class")] + [TestCase("struct")] + [TestCase("interface")] + [TestCase("record")] + [TestCase("record struct")] + [TestCase("enum")] + public async Task TestDiagnostic_FileScopedNamespace_NegativeTest_NonPublicInSupport(string typeKind) + { + string typeKindDesc = typeKind[0].ToString().ToUpper() + typeKind[1..]; + string typeName = $"Public{typeKindDesc.Replace(" ", "")}"; + + string testCode = + $""" + namespace Lucene.Net.Support.Bar; + internal {typeKind} {typeName}; + """; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestDiagnostic_FileScopedNamespace_Delegate_PositiveTest() + { + const string testCode = + """ + namespace Lucene.Net.Support; + public delegate void PublicDelegate(); + """; + + var expected = DiagnosticResult.CompilerWarning(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.Id) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments("Delegate", "PublicDelegate") + .WithLocation(2, 1); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestDiagnostic_FileScopedNamespace_Delegate_NegativeTest_PublicInAnotherNamespace() + { + const string testCode = + """ + namespace Lucene.Net.SomethingElse; + public delegate void PublicDelegate(); + """; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestDiagnostic_FileScopedNamespace_Delegate_NegativeTest_NonPublicInSupport() + { + const string testCode = + """ + namespace Lucene.Net.Support; + internal delegate void PublicDelegate(); + """; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + [TestCase("class")] + [TestCase("struct")] + [TestCase("interface")] + [TestCase("record")] + [TestCase("record struct")] + [TestCase("enum")] + public async Task TestDiagnostic_BlockScopedNamespace_PositiveTest(string typeKind) + { + string typeKindDesc = typeKind[0].ToString().ToUpper() + typeKind[1..]; + string typeName = $"Public{typeKindDesc.Replace(" ", "")}"; + + string testCode = + $$""" + namespace Lucene.Net.Support + { + public {{typeKind}} {{typeName}}; + } + """; + + var expected = DiagnosticResult.CompilerWarning(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.Id) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments(typeKindDesc, typeName) + .WithLocation(3, 5); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + [TestCase("class")] + [TestCase("struct")] + [TestCase("interface")] + [TestCase("record")] + [TestCase("record struct")] + [TestCase("enum")] + public async Task TestDiagnostic_BlockScopedNamespace_NegativeTest_PublicInAnotherNamespace(string typeKind) + { + string typeKindDesc = typeKind[0].ToString().ToUpper() + typeKind[1..]; + string typeName = $"Public{typeKindDesc.Replace(" ", "")}"; + + string testCode = + $$""" + namespace Lucene.Net.SomethingElse + { + public {{typeKind}} {{typeName}}; + } + """; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + [TestCase("class")] + [TestCase("struct")] + [TestCase("interface")] + [TestCase("record")] + [TestCase("record struct")] + [TestCase("enum")] + public async Task TestDiagnostic_BlockScopedNamespace_NegativeTest_NonPublicInSupport(string typeKind) + { + string typeKindDesc = typeKind[0].ToString().ToUpper() + typeKind[1..]; + string typeName = $"Public{typeKindDesc.Replace(" ", "")}"; + + string testCode = + $$""" + namespace Lucene.Net.Support.Foo + { + internal {{typeKind}} {{typeName}}; + } + """; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestDiagnostic_BlockScopedNamespace_Delegate_PositiveTest() + { + const string testCode = + """ + namespace Lucene.Net.Support + { + public delegate void PublicDelegate(); + } + """; + + var expected = DiagnosticResult.CompilerWarning(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.Id) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments("Delegate", "PublicDelegate") + .WithLocation(3, 5); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestDiagnostic_BlockScopedNamespace_Delegate_NegativeTest_PublicInAnotherNamespace() + { + const string testCode = + """ + namespace Lucene.Net.SomethingElse + { + public delegate void PublicDelegate(); + } + """; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestDiagnostic_BlockScopedNamespace_Delegate_NegativeTest_NonPublicInSupport() + { + const string testCode = + """ + namespace Lucene.Net.Support + { + internal delegate void PublicDelegate(); + } + """; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + } +} From 3c8150c4a4603d6fce8c5843867abf09817a4da8 Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Sat, 27 Sep 2025 21:35:09 -0600 Subject: [PATCH 02/10] Remove migrated resource for code fix title --- src/Lucene.Net.CodeAnalysis.Dev/Resources.resx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx index 4f6ecc3..8bd0b59 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx +++ b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx @@ -206,7 +206,4 @@ under the License. {0} '{1}' should not have public accessibility in the Support namespace - - Make {0} internal - From 2c17fe910602758fb0b573fe12cde7eac2302fac Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Mon, 29 Sep 2025 07:22:09 -0600 Subject: [PATCH 03/10] Remove .globalconfig file --- .globalconfig | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .globalconfig diff --git a/.globalconfig b/.globalconfig deleted file mode 100644 index de8b7b1..0000000 --- a/.globalconfig +++ /dev/null @@ -1,2 +0,0 @@ -roslyn_correctness.assembly_reference_validation = relaxed - From 5f7624f7d39aa0ae003e2e47c766234d0aabfeac Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Mon, 29 Sep 2025 21:35:04 -0600 Subject: [PATCH 04/10] Upgrade RAT to 0.16.1, clean up line endings after running --- docs/images/release-build-outcomes.md | 19 +++++++++ docs/images/release-workflow.md | 19 +++++++++ rat.ps1 | 42 +++++++++++++++---- ...uceneDev1005Sample_BlockScopedNamespace.cs | 19 +++++++++ ...LuceneDev1005Sample_FileScopedNamespace.cs | 19 +++++++++ .../AnalyzerReleases.Shipped.md | 31 +++++++++++--- .../AnalyzerReleases.Unshipped.md | 19 +++++++++ 7 files changed, 155 insertions(+), 13 deletions(-) diff --git a/docs/images/release-build-outcomes.md b/docs/images/release-build-outcomes.md index 9fcc163..a315de0 100644 --- a/docs/images/release-build-outcomes.md +++ b/docs/images/release-build-outcomes.md @@ -1,3 +1,22 @@ + + This markup can be edited and converted to .svg or .png here: https://www.mermaidchart.com/app/projects/95759d78-db93-499c-ad66-0e3f698ba88c/diagrams/31dbd6bc-7ec8-4583-a456-55e3fe3f6cfc/version/v0.1/edit diff --git a/docs/images/release-workflow.md b/docs/images/release-workflow.md index 4f1728d..c50b03d 100644 --- a/docs/images/release-workflow.md +++ b/docs/images/release-workflow.md @@ -1,3 +1,22 @@ + + This markup can be edited and converted to .svg or .png here: https://www.mermaidchart.com/app/projects/95759d78-db93-499c-ad66-0e3f698ba88c/diagrams/35faa26e-5ccf-4433-962e-32f20496471c/version/v0.1/edit diff --git a/rat.ps1 b/rat.ps1 index 15dd4c6..7281aff 100644 --- a/rat.ps1 +++ b/rat.ps1 @@ -30,7 +30,7 @@ directory as this script) as the target directory. .PARAMETER Version - The version of Apache RAT to use (default: 0.13). + The version of Apache RAT to use (default: 0.16.1). .PARAMETER ExcludeFileName Name of an exclude file containing path patterns that RAT should ignore. @@ -43,19 +43,19 @@ .EXAMPLE pwsh ./rat.ps1 - Runs Apache RAT (default version 0.13) with exclusions from `rat-exclude.txt`. + Runs Apache RAT (default version 0.16.1) with exclusions from `rat-exclude.txt`. .EXAMPLE - pwsh ./rat.ps1 -Version 0.13 -ExcludeFileName custom-exclude.txt + pwsh ./rat.ps1 -Version 0.16.1 -ExcludeFileName custom-exclude.txt - Runs Apache RAT version 0.13 using the specified exclude file. + Runs Apache RAT version 0.16.1 using the specified exclude file. .NOTES This script is intended for use by release managers when preparing official ASF releases. It is not normally required for day-to-day development. #> param( - [string]$Version = "0.13", + [string]$Version = "0.16.1", [string]$ExcludeFileName = ".rat-excludes" ) @@ -90,13 +90,13 @@ if (-not (Test-Path $ratExcludeFile)) { $argsList = @( "-jar", $ratJar, - "--dir", "`"$scriptDir`"", + "--dir", "$scriptDir", "--addLicense", "--force" ) if ($useExclude) { - $argsList += @("--exclude-file", "`"$ratExcludeFile`"") + $argsList += @("--exclude-file", "$ratExcludeFile") } # Call java with argument list. Use & to invoke program. @@ -105,3 +105,31 @@ if ($useExclude) { if ($LASTEXITCODE -ne 0) { throw "RAT exited with code $LASTEXITCODE" } + +# Remove trailing whitespace from files modified by RAT +Write-Host "Removing trailing whitespace from modified files..." + +# Get list of modified files from git +$modifiedFiles = git diff --name-only --diff-filter=M +if ($LASTEXITCODE -ne 0) { + Write-Host "Warning: Could not get modified files list from git." +} else { + foreach ($file in $modifiedFiles) { + if ([string]::IsNullOrWhiteSpace($file)) { continue } + + $filePath = Join-Path $scriptDir $file + if (-not (Test-Path $filePath)) { continue } + + try { + # Read all lines, trim trailing whitespace, and write back + $lines = Get-Content -Path $filePath + if ($null -ne $lines) { + $trimmedLines = $lines | ForEach-Object { $_.TrimEnd() } + $trimmedLines | Set-Content -Path $filePath -NoNewline:$false + Write-Host " Cleaned: $file" + } + } catch { + Write-Host " Warning: Could not process $file : $_" + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_BlockScopedNamespace.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_BlockScopedNamespace.cs index 03014da..69b19d9 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_BlockScopedNamespace.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_BlockScopedNamespace.cs @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + namespace Lucene.Net.Support.BlockScoped { public class PublicClass diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_FileScopedNamespace.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_FileScopedNamespace.cs index 2797961..57b7bc1 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_FileScopedNamespace.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_FileScopedNamespace.cs @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + namespace Lucene.Net.Support; public class PublicClass diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md index 946ff08..f76b0fa 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md +++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md @@ -1,11 +1,30 @@ + + ## Release 1.0.0-alpha.6 ### New Rules - Rule ID | Category | Severity | Notes + Rule ID | Category | Severity | Notes ---------------|----------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------- - LuceneDev1000 | Design | Warning | Floating point types should not be compared for exact equality - LuceneDev1001 | Design | Warning | Floating point types should be formatted with J2N methods - LuceneDev1002 | Design | Warning | Floating point type arithmetic needs to be checked - LuceneDev1003 | Design | Warning | Method parameters that accept array types should be analyzed to determine whether they are better suited to be ref or out parameters - LuceneDev1004 | Design | Warning | Methods that return array types should be analyzed to determine whether they are better suited to be one or more out parameters or to return a ValueTuple + LuceneDev1000 | Design | Warning | Floating point types should not be compared for exact equality + LuceneDev1001 | Design | Warning | Floating point types should be formatted with J2N methods + LuceneDev1002 | Design | Warning | Floating point type arithmetic needs to be checked + LuceneDev1003 | Design | Warning | Method parameters that accept array types should be analyzed to determine whether they are better suited to be ref or out parameters + LuceneDev1004 | Design | Warning | Methods that return array types should be analyzed to determine whether they are better suited to be one or more out parameters or to return a ValueTuple diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md index f8f5973..23855d7 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md +++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md @@ -1,3 +1,22 @@ + + ### New Rules Rule ID | Category | Severity | Notes From a09102191e5680cc25836d1cc451b4d7c9a855a1 Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Mon, 29 Sep 2025 21:37:45 -0600 Subject: [PATCH 05/10] Fix casing of code fix provider name --- ...Dev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs} | 6 +++--- ...eDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/{LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider.cs => LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs} (95%) diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs similarity index 95% rename from src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider.cs rename to src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs index feefe0e..8d2a8e9 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs @@ -31,8 +31,8 @@ namespace Lucene.Net.CodeAnalysis.Dev; -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider)), Shared] -public class LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider : CodeFixProvider +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider)), Shared] +public class LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider : CodeFixProvider { // Specify the diagnostic IDs of analyzers that are expected to be linked. public sealed override ImmutableArray FixableDiagnosticIds { get; } = @@ -79,7 +79,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) } } - private async Task MakeDeclarationInternal(Document document, + private static async Task MakeDeclarationInternal(Document document, MemberDeclarationSyntax memberDeclaration, CancellationToken cancellationToken) { diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs index 221ebad..d40f861 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs @@ -54,7 +54,7 @@ internal class MyClass var test = new InjectableCodeFixTest( () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer(), - () => new LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider()) + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider()) { TestCode = testCode.ReplaceLineEndings(), FixedCode = fixedCode.ReplaceLineEndings(), @@ -95,7 +95,7 @@ internal class MyClass var test = new InjectableCodeFixTest( () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer(), - () => new LuceneDev1005_LuceneNetSupportPublicTypesCsCodeFixProvider()) + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider()) { TestCode = testCode.ReplaceLineEndings(), FixedCode = fixedCode.ReplaceLineEndings(), From 729e2d53133cb87fe73179f1e8211aacb6090d71 Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Mon, 29 Sep 2025 22:12:07 -0600 Subject: [PATCH 06/10] Fix code fix with partial classes --- ...eNetSupportPublicTypesCSCodeFixProvider.cs | 53 +++++-- .../LuceneDev1005Sample_PartialClass1.cs | 28 ++++ .../LuceneDev1005Sample_PartialClass2.cs | 28 ++++ ...eNetSupportPublicTypesCSCodeFixProvider.cs | 148 ++++++++++++++++++ 4 files changed, 242 insertions(+), 15 deletions(-) create mode 100644 src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_PartialClass1.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_PartialClass2.cs diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs index 8d2a8e9..c722b9b 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs @@ -83,20 +83,43 @@ private static async Task MakeDeclarationInternal(Document document, MemberDeclarationSyntax memberDeclaration, CancellationToken cancellationToken) { - var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (syntaxRoot == null) return document.Project.Solution; - - // Remove existing accessibility modifiers - var newModifiers = SyntaxFactory.TokenList( - memberDeclaration.Modifiers - .Where(modifier => !modifier.IsKind(SyntaxKind.PrivateKeyword) && - !modifier.IsKind(SyntaxKind.ProtectedKeyword) && - !modifier.IsKind(SyntaxKind.InternalKeyword) && - !modifier.IsKind(SyntaxKind.PublicKeyword)) - ).Insert(0, SyntaxFactory.Token(SyntaxKind.InternalKeyword)); // Ensure 'internal' is the first modifier - - var newMemberDeclaration = memberDeclaration.WithModifiers(newModifiers); - var newRoot = syntaxRoot.ReplaceNode(memberDeclaration, newMemberDeclaration); - return document.Project.Solution.WithDocumentSyntaxRoot(document.Id, newRoot); + var solution = document.Project.Solution; + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (semanticModel == null) return solution; + + // Get the symbol for this type declaration + var symbol = semanticModel.GetDeclaredSymbol(memberDeclaration, cancellationToken); + if (symbol == null) return solution; + + // Find all partial declarations of this symbol + var declaringSyntaxReferences = symbol.DeclaringSyntaxReferences; + + // Update all partial declarations across all documents + foreach (var syntaxReference in declaringSyntaxReferences) + { + var declarationSyntax = await syntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + if (declarationSyntax is not MemberDeclarationSyntax declaration) continue; + + var declarationDocument = solution.GetDocument(syntaxReference.SyntaxTree); + if (declarationDocument == null) continue; + + var syntaxRoot = await declarationDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (syntaxRoot == null) continue; + + // Remove existing accessibility modifiers + var newModifiers = SyntaxFactory.TokenList( + declaration.Modifiers + .Where(modifier => !modifier.IsKind(SyntaxKind.PrivateKeyword) && + !modifier.IsKind(SyntaxKind.ProtectedKeyword) && + !modifier.IsKind(SyntaxKind.InternalKeyword) && + !modifier.IsKind(SyntaxKind.PublicKeyword)) + ).Insert(0, SyntaxFactory.Token(SyntaxKind.InternalKeyword)); // Ensure 'internal' is the first modifier + + var newDeclaration = declaration.WithModifiers(newModifiers); + var newRoot = syntaxRoot.ReplaceNode(declaration, newDeclaration); + solution = solution.WithDocumentSyntaxRoot(declarationDocument.Id, newRoot); + } + + return solution; } } diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_PartialClass1.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_PartialClass1.cs new file mode 100644 index 0000000..5698cf3 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_PartialClass1.cs @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +namespace Lucene.Net.Support.BlockScoped +{ + public partial class PublicPartialClass + { + public void Method1() + { + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_PartialClass2.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_PartialClass2.cs new file mode 100644 index 0000000..c8b6703 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_PartialClass2.cs @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +namespace Lucene.Net.Support.BlockScoped +{ + public partial class PublicPartialClass + { + public void Method2() + { + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs index d40f861..0e6edff 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs @@ -104,4 +104,152 @@ internal class MyClass await test.RunAsync(); } + + [Test] + public async Task PublicPartialClassInSupport_FileScopedNamespace_MakeInternalFix() + { + const string testCode1 = + """ + namespace Lucene.Net.Support; + + public partial class MyPartialClass + { + public void Method1() { } + } + """; + + const string testCode2 = + """ + namespace Lucene.Net.Support; + + public partial class MyPartialClass + { + public void Method2() { } + } + """; + + const string fixedCode1 = + """ + namespace Lucene.Net.Support; + + internal partial class MyPartialClass + { + public void Method1() { } + } + """; + + const string fixedCode2 = + """ + namespace Lucene.Net.Support; + + internal partial class MyPartialClass + { + public void Method2() { } + } + """; + + var expected1 = new DiagnosticResult(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments("Class", "MyPartialClass") + .WithLocation("/0/Test0.cs", line: 3, column: 1); + + var expected2 = new DiagnosticResult(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments("Class", "MyPartialClass") + .WithLocation("/0/Test1.cs", line: 3, column: 1); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer(), + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider()) + { + ExpectedDiagnostics = { expected1, expected2 }, + // Skip FixAll to test single fix application only + CodeFixTestBehaviors = CodeFixTestBehaviors.FixOne + }; + + test.TestState.Sources.Add(testCode1.ReplaceLineEndings()); + test.TestState.Sources.Add(testCode2.ReplaceLineEndings()); + test.FixedState.Sources.Add(fixedCode1.ReplaceLineEndings()); + test.FixedState.Sources.Add(fixedCode2.ReplaceLineEndings()); + + await test.RunAsync(); + } + + [Test] + public async Task PublicPartialClassInSupport_BlockScopedNamespace_MakeInternalFix() + { + const string testCode1 = + """ + namespace Lucene.Net.Support + { + public partial class MyPartialClass + { + public void Method1() { } + } + } + """; + + const string testCode2 = + """ + namespace Lucene.Net.Support + { + public partial class MyPartialClass + { + public void Method2() { } + } + } + """; + + const string fixedCode1 = + """ + namespace Lucene.Net.Support + { + internal partial class MyPartialClass + { + public void Method1() { } + } + } + """; + + const string fixedCode2 = + """ + namespace Lucene.Net.Support + { + internal partial class MyPartialClass + { + public void Method2() { } + } + } + """; + + var expected1 = new DiagnosticResult(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments("Class", "MyPartialClass") + .WithLocation("/0/Test0.cs", line: 3, column: 5); + + var expected2 = new DiagnosticResult(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments("Class", "MyPartialClass") + .WithLocation("/0/Test1.cs", line: 3, column: 5); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer(), + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider()) + { + ExpectedDiagnostics = { expected1, expected2 }, + // Skip FixAll to test single fix application only + CodeFixTestBehaviors = CodeFixTestBehaviors.FixOne + }; + + test.TestState.Sources.Add(testCode1.ReplaceLineEndings()); + test.TestState.Sources.Add(testCode2.ReplaceLineEndings()); + test.FixedState.Sources.Add(fixedCode1.ReplaceLineEndings()); + test.FixedState.Sources.Add(fixedCode2.ReplaceLineEndings()); + + await test.RunAsync(); + } } From fe580380a52604ab896ad829c943277e42b60e3a Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Mon, 29 Sep 2025 22:17:17 -0600 Subject: [PATCH 07/10] Revert Markdown changes and exclude from RAT --- .rat-excludes | 3 ++ docs/images/release-build-outcomes.md | 19 ------------ docs/images/release-workflow.md | 19 ------------ .../AnalyzerReleases.Shipped.md | 31 ++++--------------- .../AnalyzerReleases.Unshipped.md | 19 ------------ 5 files changed, 9 insertions(+), 82 deletions(-) diff --git a/.rat-excludes b/.rat-excludes index 1e058e4..1c529fc 100644 --- a/.rat-excludes +++ b/.rat-excludes @@ -20,3 +20,6 @@ _site/* release-build-outcomes\.svg release-workflow\.svg + +# Exclude markdown files +.*\.md diff --git a/docs/images/release-build-outcomes.md b/docs/images/release-build-outcomes.md index a315de0..9fcc163 100644 --- a/docs/images/release-build-outcomes.md +++ b/docs/images/release-build-outcomes.md @@ -1,22 +1,3 @@ - - This markup can be edited and converted to .svg or .png here: https://www.mermaidchart.com/app/projects/95759d78-db93-499c-ad66-0e3f698ba88c/diagrams/31dbd6bc-7ec8-4583-a456-55e3fe3f6cfc/version/v0.1/edit diff --git a/docs/images/release-workflow.md b/docs/images/release-workflow.md index c50b03d..4f1728d 100644 --- a/docs/images/release-workflow.md +++ b/docs/images/release-workflow.md @@ -1,22 +1,3 @@ - - This markup can be edited and converted to .svg or .png here: https://www.mermaidchart.com/app/projects/95759d78-db93-499c-ad66-0e3f698ba88c/diagrams/35faa26e-5ccf-4433-962e-32f20496471c/version/v0.1/edit diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md index f76b0fa..946ff08 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md +++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md @@ -1,30 +1,11 @@ - - ## Release 1.0.0-alpha.6 ### New Rules - Rule ID | Category | Severity | Notes + Rule ID | Category | Severity | Notes ---------------|----------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------- - LuceneDev1000 | Design | Warning | Floating point types should not be compared for exact equality - LuceneDev1001 | Design | Warning | Floating point types should be formatted with J2N methods - LuceneDev1002 | Design | Warning | Floating point type arithmetic needs to be checked - LuceneDev1003 | Design | Warning | Method parameters that accept array types should be analyzed to determine whether they are better suited to be ref or out parameters - LuceneDev1004 | Design | Warning | Methods that return array types should be analyzed to determine whether they are better suited to be one or more out parameters or to return a ValueTuple + LuceneDev1000 | Design | Warning | Floating point types should not be compared for exact equality + LuceneDev1001 | Design | Warning | Floating point types should be formatted with J2N methods + LuceneDev1002 | Design | Warning | Floating point type arithmetic needs to be checked + LuceneDev1003 | Design | Warning | Method parameters that accept array types should be analyzed to determine whether they are better suited to be ref or out parameters + LuceneDev1004 | Design | Warning | Methods that return array types should be analyzed to determine whether they are better suited to be one or more out parameters or to return a ValueTuple diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md index 23855d7..f8f5973 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md +++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md @@ -1,22 +1,3 @@ - - ### New Rules Rule ID | Category | Severity | Notes From 13a7c868724502588fed28d041e0578a94134595 Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Tue, 30 Sep 2025 09:17:46 -0600 Subject: [PATCH 08/10] RAT exclude only AnalyzerReleases files; fix warning about AnalyzerReleases version number --- .rat-excludes | 5 +++-- docs/images/release-build-outcomes.md | 19 +++++++++++++++++++ docs/images/release-workflow.md | 19 +++++++++++++++++++ docs/make-release.md | 6 +++++- eng/git-hooks/post-commit | 4 ++-- .../AnalyzerReleases.Shipped.md | 2 +- 6 files changed, 49 insertions(+), 6 deletions(-) diff --git a/.rat-excludes b/.rat-excludes index 1c529fc..c0d0812 100644 --- a/.rat-excludes +++ b/.rat-excludes @@ -21,5 +21,6 @@ _site/* release-build-outcomes\.svg release-workflow\.svg -# Exclude markdown files -.*\.md +# Exclude analyzer releases markdown files +AnalyzerReleases\..*\.md + diff --git a/docs/images/release-build-outcomes.md b/docs/images/release-build-outcomes.md index 9fcc163..a315de0 100644 --- a/docs/images/release-build-outcomes.md +++ b/docs/images/release-build-outcomes.md @@ -1,3 +1,22 @@ + + This markup can be edited and converted to .svg or .png here: https://www.mermaidchart.com/app/projects/95759d78-db93-499c-ad66-0e3f698ba88c/diagrams/31dbd6bc-7ec8-4583-a456-55e3fe3f6cfc/version/v0.1/edit diff --git a/docs/images/release-workflow.md b/docs/images/release-workflow.md index 4f1728d..c50b03d 100644 --- a/docs/images/release-workflow.md +++ b/docs/images/release-workflow.md @@ -1,3 +1,22 @@ + + This markup can be edited and converted to .svg or .png here: https://www.mermaidchart.com/app/projects/95759d78-db93-499c-ad66-0e3f698ba88c/diagrams/35faa26e-5ccf-4433-962e-32f20496471c/version/v0.1/edit diff --git a/docs/make-release.md b/docs/make-release.md index 0ec19bf..3ea7458 100644 --- a/docs/make-release.md +++ b/docs/make-release.md @@ -282,8 +282,12 @@ Since Nerdbank.GitVersioning calculates the release version, the `AnalyzerReleas `AnalyzerReleases.Shipped.md` evolves by appending each release as a new section. Each release is marked with a `## Release ` header. +> [!NOTE] +> Due to a limitation/bug in the Roslyn meta-analyzers, the Release header cannot contain semver pre-release labels (e.g., `-beta`, `-rc`, etc.). +> Therefore, the version token `{{vnext}}` is used to indicate the next release version, which will be replaced with the actual version during the release process. + ```markdown -## Release 2.0.0-alpha.1 +## Release 2.0.0 ### New Rules diff --git a/eng/git-hooks/post-commit b/eng/git-hooks/post-commit index 5918708..eb00e82 100755 --- a/eng/git-hooks/post-commit +++ b/eng/git-hooks/post-commit @@ -56,8 +56,8 @@ fi # Set flag to prevent recursion export POST_COMMIT_RUNNING=1 -# Get the NuGet version -version=$(nbgv get-version -v NuGetPackageVersion) +# Get the NuGet version without any prerelease suffix, which is not compatible with the Roslyn meta-analyzers +version=$(nbgv get-version -v Version) echo "Replacing '$token' with '$version' in '$file'" # Replace {{vnext}} only in lines starting with "## Release " diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md index 946ff08..555dd14 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md +++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md @@ -1,4 +1,4 @@ -## Release 1.0.0-alpha.6 +## Release 1.0.0 ### New Rules From ca536ffd9cd4058615880f0448a84b9c170b4b4a Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Tue, 30 Sep 2025 09:32:30 -0600 Subject: [PATCH 09/10] Use MajorMinorVersion instead of Version --- docs/make-release.md | 4 ++-- eng/git-hooks/post-commit | 6 +++--- src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/make-release.md b/docs/make-release.md index 3ea7458..1db5fc9 100644 --- a/docs/make-release.md +++ b/docs/make-release.md @@ -284,10 +284,10 @@ Since Nerdbank.GitVersioning calculates the release version, the `AnalyzerReleas > [!NOTE] > Due to a limitation/bug in the Roslyn meta-analyzers, the Release header cannot contain semver pre-release labels (e.g., `-beta`, `-rc`, etc.). -> Therefore, the version token `{{vnext}}` is used to indicate the next release version, which will be replaced with the actual version during the release process. +> 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. ```markdown -## Release 2.0.0 +## Release 2.0 ### New Rules diff --git a/eng/git-hooks/post-commit b/eng/git-hooks/post-commit index eb00e82..7f8bd40 100755 --- a/eng/git-hooks/post-commit +++ b/eng/git-hooks/post-commit @@ -45,10 +45,10 @@ The 'nbgv' tool is required but not installed. To recover manually: 1. Install the nbgv tool (see the docs/make-release.md documentation) -2. Run: nbgv get-version -v NuGetPackageVersion +2. Run: nbgv get-version -v MajorMinorVersion 3. In $file, replace the $token token with the version returned from step 2 4. Run: git add $file && git commit --amend --no-edit -5. Run: nbgv get-version -v NuGetPackageVersion again to ensure the version is the same as step 2 before proceeding +5. Run: nbgv get-version -v MajorMinorVersion again to ensure the version is the same as step 2 before proceeding EOF exit 1 fi @@ -57,7 +57,7 @@ fi export POST_COMMIT_RUNNING=1 # Get the NuGet version without any prerelease suffix, which is not compatible with the Roslyn meta-analyzers -version=$(nbgv get-version -v Version) +version=$(nbgv get-version -v MajorMinorVersion) echo "Replacing '$token' with '$version' in '$file'" # Replace {{vnext}} only in lines starting with "## Release " diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md index 555dd14..e52b56b 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md +++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md @@ -1,4 +1,4 @@ -## Release 1.0.0 +## Release 1.0 ### New Rules From bd72042e2a52376d3892cfbce3683c2c9fecb1f8 Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Sat, 4 Oct 2025 14:59:13 -0600 Subject: [PATCH 10/10] Fix code fix to preserve license headers and comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...eNetSupportPublicTypesCSCodeFixProvider.cs | 15 +- ...eNetSupportPublicTypesCSCodeFixProvider.cs | 189 ++++++++++++++++++ 2 files changed, 203 insertions(+), 1 deletion(-) diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs index c722b9b..30a24f0 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs @@ -106,6 +106,19 @@ private static async Task MakeDeclarationInternal(Document document, var syntaxRoot = await declarationDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (syntaxRoot == null) continue; + // Get leading trivia from the first modifier (which contains license headers/comments) + var leadingTrivia = declaration.Modifiers.Count > 0 + ? declaration.Modifiers[0].LeadingTrivia + : SyntaxTriviaList.Empty; + + // Get trailing trivia from the accessibility modifier we're removing (typically whitespace) + var accessibilityModifier = declaration.Modifiers + .FirstOrDefault(m => m.IsKind(SyntaxKind.PublicKeyword) || + m.IsKind(SyntaxKind.InternalKeyword) || + m.IsKind(SyntaxKind.ProtectedKeyword) || + m.IsKind(SyntaxKind.PrivateKeyword)); + var trailingTrivia = accessibilityModifier.TrailingTrivia; + // Remove existing accessibility modifiers var newModifiers = SyntaxFactory.TokenList( declaration.Modifiers @@ -113,7 +126,7 @@ private static async Task MakeDeclarationInternal(Document document, !modifier.IsKind(SyntaxKind.ProtectedKeyword) && !modifier.IsKind(SyntaxKind.InternalKeyword) && !modifier.IsKind(SyntaxKind.PublicKeyword)) - ).Insert(0, SyntaxFactory.Token(SyntaxKind.InternalKeyword)); // Ensure 'internal' is the first modifier + ).Insert(0, SyntaxFactory.Token(leadingTrivia, SyntaxKind.InternalKeyword, trailingTrivia)); // Ensure 'internal' is the first modifier with preserved trivia var newDeclaration = declaration.WithModifiers(newModifiers); var newRoot = syntaxRoot.ReplaceNode(declaration, newDeclaration); diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs index 0e6edff..a9e759e 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs @@ -252,4 +252,193 @@ public void Method2() { } await test.RunAsync(); } + + [Test] + public async Task PublicTypeInSupport_WithLicenseHeader_PreservesHeader() + { + const string testCode = + """ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + + namespace Lucene.Net.Support; + + public class MyClass + { + } + """; + + const string fixedCode = + """ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + + namespace Lucene.Net.Support; + + internal class MyClass + { + } + """; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments("Class", "MyClass") + .WithLocation(20, 1); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer(), + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider()) + { + TestCode = testCode.ReplaceLineEndings(), + FixedCode = fixedCode.ReplaceLineEndings(), + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task PublicTypeInSupport_WithLicenseHeaderInsideNamespace_PreservesHeader() + { + const string testCode = + """ + namespace Lucene.Net.Support + { + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + + public class MyClass + { + } + } + """; + + const string fixedCode = + """ + namespace Lucene.Net.Support + { + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + + internal class MyClass + { + } + } + """; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments("Class", "MyClass") + .WithLocation(20, 5); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer(), + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider()) + { + TestCode = testCode.ReplaceLineEndings(), + FixedCode = fixedCode.ReplaceLineEndings(), + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task PublicTypeInSupport_WithTrailingTrivia_PreservesTrailingTrivia() + { + const string testCode = + """ + namespace Lucene.Net.Support + { + public class MyClass + { + } // Important trailing comment + } + """; + + const string fixedCode = + """ + namespace Lucene.Net.Support + { + internal class MyClass + { + } // Important trailing comment + } + """; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1005_LuceneNetSupportPublicTypes.MessageFormat) + .WithArguments("Class", "MyClass") + .WithLocation(3, 5); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeAnalyzer(), + () => new LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider()) + { + TestCode = testCode.ReplaceLineEndings(), + FixedCode = fixedCode.ReplaceLineEndings(), + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } }