diff --git a/.rat-excludes b/.rat-excludes index 1e058e4..c0d0812 100644 --- a/.rat-excludes +++ b/.rat-excludes @@ -20,3 +20,7 @@ _site/* release-build-outcomes\.svg release-workflow\.svg + +# 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..1db5fc9 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 minor release version, which will be replaced with the actual version during the git post-commit hook. + ```markdown -## Release 2.0.0-alpha.1 +## Release 2.0 ### New Rules diff --git a/eng/git-hooks/post-commit b/eng/git-hooks/post-commit index 5918708..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 @@ -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 MajorMinorVersion) echo "Replacing '$token' with '$version' in '$file'" # Replace {{vnext}} only in lines starting with "## Release " 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.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..30a24f0 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs @@ -0,0 +1,138 @@ +/* + * 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 static async Task MakeDeclarationInternal(Document document, + MemberDeclarationSyntax memberDeclaration, + CancellationToken cancellationToken) + { + 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; + + // 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 + .Where(modifier => !modifier.IsKind(SyntaxKind.PrivateKeyword) && + !modifier.IsKind(SyntaxKind.ProtectedKeyword) && + !modifier.IsKind(SyntaxKind.InternalKeyword) && + !modifier.IsKind(SyntaxKind.PublicKeyword)) + ).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); + solution = solution.WithDocumentSyntaxRoot(declarationDocument.Id, newRoot); + } + + return solution; + } +} 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..69b19d9 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_BlockScopedNamespace.cs @@ -0,0 +1,47 @@ +/* + * 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 + { + } + + 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..57b7bc1 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1005Sample_FileScopedNamespace.cs @@ -0,0 +1,46 @@ +/* + * 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 +{ +} + +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_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/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Shipped.md index 946ff08..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-alpha.6 +## Release 1.0 ### New Rules 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..8bd0b59 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx +++ b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx @@ -197,4 +197,13 @@ 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 + 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..a9e759e --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1005_LuceneNetSupportPublicTypesCSCodeFixProvider.cs @@ -0,0 +1,444 @@ +/* + * 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(); + } + + [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(); + } + + [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(); + } +} 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(); + } + } +}