├── Immutable ├── Apex.Analyzers.Immutable │ ├── Apex.Analyzers.Immutable.snk │ ├── AnalyzerReleases.Unshipped.md │ ├── AnalyzerReleases.Shipped.md │ ├── ApexAnalyzersImmutableAnalyzer.cs │ ├── tools │ │ ├── install.ps1 │ │ └── uninstall.ps1 │ ├── Rules │ │ ├── IMM001.cs │ │ ├── IMM006.cs │ │ ├── IMM002.cs │ │ ├── IMM007.cs │ │ ├── IMM003.cs │ │ ├── IMM004.cs │ │ ├── IMM005.cs │ │ └── IMM008.cs │ ├── Apex.Analyzers.Immutable.csproj │ ├── ApexAnalyzersImmutableCodeFixProvider.cs │ ├── Resources.resx │ └── Resources.Designer.cs ├── Apex.Analyzers.Immutable.Semantics │ ├── Apex.Analyzers.Immutable.Semantics.snk │ ├── AssemblyInfo.cs │ ├── Apex.Analyzers.Immutable.Semantics.csproj │ ├── Helper.cs │ └── ImmutableTypes.cs ├── Apex.Analyzers.Immutable.Attributes │ ├── Apex.Analyzers.Immutable.Attributes.snk │ ├── ImmutableAttribute.cs │ └── Apex.Analyzers.Immutable.Attributes.csproj ├── Apex.Analyzers.Immutable.Test │ ├── Helpers │ │ ├── AdditionalFile.cs │ │ └── DiagnosticResult.cs │ ├── Apex.Analyzers.Immutable.Test.csproj │ ├── Verifiers │ │ └── CSharpCodeFixVerifier.cs │ └── ApexAnalyzersImmutableUnitTests.cs └── Apex.Analyzers.Immutable.Vsix │ ├── source.extension.vsixmanifest │ └── Apex.Analyzers.Immutable.Vsix.csproj ├── .gitignore ├── .github └── dependabot.yml ├── azure-pipelines.yml ├── LICENSE ├── README.md └── Apex.Analyzers.sln /Immutable/Apex.Analyzers.Immutable/Apex.Analyzers.Immutable.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbolin/Apex.Analyzers/HEAD/Immutable/Apex.Analyzers.Immutable/Apex.Analyzers.Immutable.snk -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Semantics/Apex.Analyzers.Immutable.Semantics.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbolin/Apex.Analyzers/HEAD/Immutable/Apex.Analyzers.Immutable.Semantics/Apex.Analyzers.Immutable.Semantics.snk -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Attributes/Apex.Analyzers.Immutable.Attributes.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbolin/Apex.Analyzers/HEAD/Immutable/Apex.Analyzers.Immutable.Attributes/Apex.Analyzers.Immutable.Attributes.snk -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/AnalyzerReleases.Unshipped.md: -------------------------------------------------------------------------------- 1 | ; Unshipped analyzer release 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio. 3 | ################################################################################ 4 | 5 | /.vs/Apex.Analyzers 6 | obj 7 | bin 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: Microsoft.NET.Test.Sdk 10 | versions: 11 | - 16.9.1 12 | - dependency-name: Microsoft.CodeAnalysis.CSharp.Workspaces 13 | versions: 14 | - 3.8.0 15 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Semantics/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Apex.Analyzers.Immutable, PublicKey=00240000048000009400000006020000002400005253413100040000010001007d19c824a9ae1ddca21805b977daedd3ccd3d953d25064682938267d27bec91a78f010fe97b63e8b37f887156d9697435bef34ad4c274afac9e6d76b18afd5516980f396a4e8048191e40a2d9a0a2c42ecbc6d4194e792db6b00efbd910a825c552cb8a5f5e9de1da618dc688c0200b9e146a6fe7dde29873e1c9e0eba4456bf")] -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Attributes/ImmutableAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace System 2 | { 3 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] 4 | public sealed class ImmutableAttribute : Attribute 5 | { 6 | public ImmutableAttribute(bool onFaith = false) 7 | { 8 | OnFaith = onFaith; 9 | } 10 | 11 | public bool OnFaith { get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Test/Helpers/AdditionalFile.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | using System.Threading; 4 | 5 | namespace TestHelper 6 | { 7 | public class AdditionalFile : AdditionalText 8 | { 9 | public AdditionalFile(string path, string text) 10 | { 11 | Path = path; 12 | Text = text; 13 | } 14 | 15 | public override string Path { get; } 16 | public string Text { get; } 17 | 18 | public override SourceText GetText(CancellationToken cancellationToken = default) 19 | { 20 | return SourceText.From(Text); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | pool: 2 | name: Default 3 | variables: 4 | BuildConfiguration: 'Release' 5 | 6 | steps: 7 | - script: 'dotnet build Immutable/Apex.Analyzers.Immutable/Apex.Analyzers.Immutable.csproj -c $(BuildConfiguration)' 8 | displayName: 'dotnet build' 9 | - task: DeleteFiles@1 10 | inputs: 11 | Contents: '**/*.trx' 12 | - script: | 13 | dotnet test Immutable/Apex.Analyzers.Immutable.Test/Apex.Analyzers.Immutable.Test.csproj -c $(BuildConfiguration) --logger "trx;LogFileName=results.trx" 14 | displayName: 'dotnet test' 15 | 16 | - task: PublishTestResults@2 17 | inputs: 18 | testResultsFormat: 'VSTest' 19 | testResultsFiles: '**/results*.trx' 20 | mergeTestResults: true 21 | failTaskOnFailedTests: true 22 | testRunTitle: 'Unit tests' 23 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Test/Apex.Analyzers.Immutable.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dominic Bolin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/AnalyzerReleases.Shipped.md: -------------------------------------------------------------------------------- 1 | ; Shipped analyzer releases 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | 4 | ## Release 1.0 5 | 6 | ### New Rules 7 | 8 | Rule ID | Category | Severity | Notes 9 | --------|----------|----------|-------------------- 10 | IMM001 | Architecture | Error | Fields in an immutable type must be readonly 11 | IMM002 | Architecture | Error | Auto properties in an immutable type must not define a set method 12 | IMM003 | Architecture | Error | Types of fields in an immutable type must be immutable 13 | IMM004 | Architecture | Error | Types of auto properties in an immutable type must be immutable 14 | IMM005 | Architecture | Warning | 'This' should not be passed out of the constructor of an immutable type 15 | IMM006 | Architecture | Error | The base type of an immutable type must be 'object' or immutable 16 | IMM007 | Architecture | Error | Types derived from an immutable type must be immutable 17 | IMM008 | Architecture | Warning | 'This' should not be passed out of an init only property method of an immutable type 18 | 19 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Attributes/Apex.Analyzers.Immutable.Attributes.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | True 6 | 7 | 8 | 9 | Apex.Analyzers.Immutable.Attributes 10 | 1.0.1 11 | Dominic Bolin 12 | MIT 13 | https://github.com/dbolin/Apex.Analyzers 14 | https://github.com/dbolin/Apex.Analyzers 15 | true 16 | false 17 | Attributes used by Apex.Analyzers.Immutable 18 | Copyright (c) 2019 Dominic Bolin 19 | Apex.Analyzers.Immutable, immutable, architecture, design, csharp, analyzers 20 | true 21 | true 22 | Apex.Analyzers.Immutable.Attributes.snk 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/ApexAnalyzersImmutableAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Apex.Analyzers.Immutable.Rules; 3 | using Apex.Analyzers.Immutable.Semantics; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | 7 | namespace Apex.Analyzers.Immutable 8 | { 9 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 10 | public class ApexAnalyzersImmutableAnalyzer : DiagnosticAnalyzer 11 | { 12 | public override ImmutableArray SupportedDiagnostics 13 | { 14 | get 15 | { 16 | return ImmutableArray.Create(IMM001.Rule, IMM002.Rule, IMM003.Rule, IMM004.Rule, IMM005.Rule, IMM006.Rule, IMM007.Rule, IMM008.Rule); 17 | } 18 | } 19 | 20 | public override void Initialize(AnalysisContext context) 21 | { 22 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 23 | context.EnableConcurrentExecution(); 24 | 25 | var whitelist = new ImmutableTypes(); 26 | IMM001.Initialize(context); 27 | IMM002.Initialize(context); 28 | IMM003.Initialize(context, whitelist); 29 | IMM004.Initialize(context, whitelist); 30 | IMM005.Initialize(context); 31 | IMM006.Initialize(context, whitelist); 32 | IMM007.Initialize(context); 33 | IMM008.Initialize(context); 34 | } 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Vsix/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Apex.Analyzers.Immutable 6 | This is a sample diagnostic extension for the .NET Compiler Platform ("Roslyn"). 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Semantics/Apex.Analyzers.Immutable.Semantics.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | True 6 | 7 | 8 | 9 | Apex.Analyzers.Immutable.Semantics 10 | 1.0.3 11 | Dominic Bolin 12 | MIT 13 | https://github.com/dbolin/Apex.Analyzers 14 | https://github.com/dbolin/Apex.Analyzers 15 | true 16 | false 17 | Semantics used by Apex.Analyzers.Immutable 18 | Copyright (c) 2019 Dominic Bolin 19 | Apex.Analyzers.Immutable, immutable, architecture, design, csharp, analyzers 20 | true 21 | true 22 | Apex.Analyzers.Immutable.Semantics.snk 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/tools/install.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not install analyzers via install.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | if (Test-Path $analyzersPath) 17 | { 18 | # Install the language agnostic analyzers. 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Install language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apex.Analyzers 2 | Roslyn powered analyzers for C# to support convention defined architecture 3 | 4 | ## Immutable Types 5 | 6 | [![Build Status](https://numenfall.visualstudio.com/Games/_apis/build/status/Apex.Analyzers-CI?branchName=master)](https://numenfall.visualstudio.com/Games/_build/latest?definitionId=5&branchName=master) 7 | 8 | [Nuget Package](https://www.nuget.org/packages/Apex.Analyzers.Immutable/) 9 | 10 | Provides an `ImmutableAttribute` type which can be applied to classes, structs, and interfaces. The analyzer ensures that the following rules hold for types marked with the attribute. 11 | 12 | | ID | Severity | Rule | Code Fix 13 | | --- | --- | --- | --- | 14 | | `IMM001` | Error | Fields in an immutable type must be readonly | Yes | 15 | | `IMM002` | Error | Auto properties in an immutable type must not define a set method | Yes | 16 | | `IMM003` | Error | Types of fields in an immutable type must be immutable | No | 17 | | `IMM004` | Error | Types of auto properties in an immutable type must be immutable | No | 18 | | `IMM005` | Warning | 'This' should not be passed out of the constructor of an immutable type | No | 19 | | `IMM006` | Error | The base type of an immutable type must be 'object' or immutable | No | 20 | | `IMM007` | Error | Types derived from an immutable type must be immutable | No | 21 | | `IMM008` | Warning | 'This' should not be passed out of an init only property method of an immutable type | No | 22 | 23 | ### Whitelisting types via additional files 24 | 25 | The immutable types analyzer allows specifying types to be whitelisted in an [Additional File](https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Using%20Additional%20Files.md). 26 | 27 | The name of the additional file is "ImmutableTypes.txt" and the format of the file is one namespace qualified type name per line. 28 | For example: 29 | ``` 30 | System.Xml.Linq.XName 31 | System.Func`1 32 | ``` 33 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/tools/uninstall.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | # Uninstall the language agnostic analyzers. 17 | if (Test-Path $analyzersPath) 18 | { 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Uninstall language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | try 55 | { 56 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 57 | } 58 | catch 59 | { 60 | 61 | } 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/Rules/IMM001.cs: -------------------------------------------------------------------------------- 1 | using Apex.Analyzers.Immutable.Semantics; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | 5 | namespace Apex.Analyzers.Immutable.Rules 6 | { 7 | internal static class IMM001 8 | { 9 | public const string DiagnosticId = "IMM001"; 10 | 11 | private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.IMM001Title), Resources.ResourceManager, typeof(Resources)); 12 | private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.IMM001MessageFormat), Resources.ResourceManager, typeof(Resources)); 13 | 14 | private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.IMM001Description), Resources.ResourceManager, typeof(Resources)); 15 | private const string Category = "Architecture"; 16 | 17 | public static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); 18 | internal static void Initialize(AnalysisContext context) 19 | { 20 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 21 | context.EnableConcurrentExecution(); 22 | context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Field); 23 | } 24 | 25 | private static void AnalyzeSymbol(SymbolAnalysisContext context) 26 | { 27 | var symbol = (IFieldSymbol)context.Symbol; 28 | var containingType = symbol.ContainingType; 29 | if(containingType == null) 30 | { 31 | return; 32 | } 33 | 34 | if(Helper.HasImmutableAttributeAndShouldVerify(containingType) 35 | && !symbol.IsReadOnly 36 | && Helper.ShouldCheckMemberTypeForImmutability(symbol)) 37 | { 38 | var diagnostic = Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name); 39 | context.ReportDiagnostic(diagnostic); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/Rules/IMM006.cs: -------------------------------------------------------------------------------- 1 | using Apex.Analyzers.Immutable.Semantics; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | 5 | namespace Apex.Analyzers.Immutable.Rules 6 | { 7 | internal static class IMM006 8 | { 9 | public const string DiagnosticId = "IMM006"; 10 | 11 | private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.IMM006Title), Resources.ResourceManager, typeof(Resources)); 12 | private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.IMM006MessageFormat), Resources.ResourceManager, typeof(Resources)); 13 | 14 | private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.IMM006Description), Resources.ResourceManager, typeof(Resources)); 15 | private const string Category = "Architecture"; 16 | 17 | public static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); 18 | internal static void Initialize(AnalysisContext context, ImmutableTypes immutableTypes) 19 | { 20 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 21 | context.EnableConcurrentExecution(); 22 | context.RegisterSymbolAction(x => AnalyzeSymbol(x, immutableTypes), SymbolKind.NamedType); 23 | } 24 | 25 | private static void AnalyzeSymbol(SymbolAnalysisContext context, ImmutableTypes immutableTypes) 26 | { 27 | immutableTypes.Initialize(context.Compilation, context.Options, context.CancellationToken); 28 | 29 | string genericTypeArgument = null; 30 | var symbol = (INamedTypeSymbol)context.Symbol; 31 | if (symbol.BaseType != null 32 | && Helper.HasImmutableAttributeAndShouldVerify(symbol) 33 | && !immutableTypes.IsImmutableType(symbol.BaseType, ref genericTypeArgument)) 34 | { 35 | var diagnostic = Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name); 36 | context.ReportDiagnostic(diagnostic); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/Rules/IMM002.cs: -------------------------------------------------------------------------------- 1 | using Apex.Analyzers.Immutable.Semantics; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | using System.Linq; 5 | 6 | namespace Apex.Analyzers.Immutable.Rules 7 | { 8 | internal static class IMM002 9 | { 10 | public const string DiagnosticId = "IMM002"; 11 | 12 | private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.IMM002Title), Resources.ResourceManager, typeof(Resources)); 13 | private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.IMM002MessageFormat), Resources.ResourceManager, typeof(Resources)); 14 | 15 | private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.IMM002Description), Resources.ResourceManager, typeof(Resources)); 16 | private const string Category = "Architecture"; 17 | 18 | public static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); 19 | internal static void Initialize(AnalysisContext context) 20 | { 21 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 22 | context.EnableConcurrentExecution(); 23 | context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Property); 24 | } 25 | 26 | private static void AnalyzeSymbol(SymbolAnalysisContext context) 27 | { 28 | var symbol = (IPropertySymbol)context.Symbol; 29 | var containingType = symbol.ContainingType; 30 | if(containingType == null) 31 | { 32 | return; 33 | } 34 | 35 | if(Helper.HasImmutableAttributeAndShouldVerify(containingType) 36 | && !symbol.IsReadOnly 37 | && (symbol.SetMethod == null || !Helper.IsInitOnlyMethod(symbol.SetMethod)) 38 | && Helper.ShouldCheckMemberTypeForImmutability(symbol) 39 | && Helper.IsAutoProperty(symbol)) 40 | { 41 | var diagnostic = Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name); 42 | context.ReportDiagnostic(diagnostic); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Test/Helpers/DiagnosticResult.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System; 3 | 4 | namespace TestHelper 5 | { 6 | /// 7 | /// Location where the diagnostic appears, as determined by path, line number, and column number. 8 | /// 9 | public struct DiagnosticResultLocation 10 | { 11 | public DiagnosticResultLocation(string path, int line, int column) 12 | { 13 | if (line < -1) 14 | { 15 | throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); 16 | } 17 | 18 | if (column < -1) 19 | { 20 | throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); 21 | } 22 | 23 | this.Path = path; 24 | this.Line = line; 25 | this.Column = column; 26 | } 27 | 28 | public string Path { get; } 29 | public int Line { get; } 30 | public int Column { get; } 31 | } 32 | 33 | /// 34 | /// Struct that stores information about a Diagnostic appearing in a source 35 | /// 36 | public struct DiagnosticResult 37 | { 38 | private DiagnosticResultLocation[] locations; 39 | 40 | public DiagnosticResultLocation[] Locations 41 | { 42 | get 43 | { 44 | if (this.locations == null) 45 | { 46 | this.locations = new DiagnosticResultLocation[] { }; 47 | } 48 | return this.locations; 49 | } 50 | 51 | set 52 | { 53 | this.locations = value; 54 | } 55 | } 56 | 57 | public DiagnosticSeverity Severity { get; set; } 58 | 59 | public string Id { get; set; } 60 | 61 | public string Message { get; set; } 62 | 63 | public string Path 64 | { 65 | get 66 | { 67 | return this.Locations.Length > 0 ? this.Locations[0].Path : ""; 68 | } 69 | } 70 | 71 | public int Line 72 | { 73 | get 74 | { 75 | return this.Locations.Length > 0 ? this.Locations[0].Line : -1; 76 | } 77 | } 78 | 79 | public int Column 80 | { 81 | get 82 | { 83 | return this.Locations.Length > 0 ? this.Locations[0].Column : -1; 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/Rules/IMM007.cs: -------------------------------------------------------------------------------- 1 | using Apex.Analyzers.Immutable.Semantics; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | using System.Linq; 5 | 6 | namespace Apex.Analyzers.Immutable.Rules 7 | { 8 | internal static class IMM007 9 | { 10 | public const string DiagnosticId = "IMM007"; 11 | 12 | private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.IMM007Title), Resources.ResourceManager, typeof(Resources)); 13 | private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.IMM007MessageFormat), Resources.ResourceManager, typeof(Resources)); 14 | 15 | private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.IMM007Description), Resources.ResourceManager, typeof(Resources)); 16 | private const string Category = "Architecture"; 17 | 18 | public static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); 19 | internal static void Initialize(AnalysisContext context) 20 | { 21 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 22 | context.EnableConcurrentExecution(); 23 | context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType); 24 | } 25 | 26 | private static void AnalyzeSymbol(SymbolAnalysisContext context) 27 | { 28 | var symbol = (INamedTypeSymbol)context.Symbol; 29 | if (!Helper.HasImmutableAttribute(symbol)) 30 | { 31 | var baseTypeName = Helper.HasImmutableAttribute(symbol.BaseType) ? symbol.BaseType.Name : null; 32 | var interfaceName = symbol.AllInterfaces.FirstOrDefault(x => Helper.HasImmutableAttribute(x))?.Name; 33 | 34 | if (baseTypeName != null) 35 | { 36 | var diagnostic = Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name, baseTypeName); 37 | context.ReportDiagnostic(diagnostic); 38 | } 39 | else if(interfaceName != null) 40 | { 41 | var diagnostic = Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name, interfaceName); 42 | context.ReportDiagnostic(diagnostic); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/Apex.Analyzers.Immutable.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | false 6 | 7 | 8 | 9 | Apex.Analyzers.Immutable 10 | 1.2.7 11 | Dominic Bolin 12 | MIT 13 | https://github.com/dbolin/Apex.Analyzers 14 | https://github.com/dbolin/Apex.Analyzers 15 | true 16 | false 17 | Roslyn powered analyzers for C# to support defining immutable types 18 | Copyright (c) 2019 Dominic Bolin 19 | Apex.Analyzers.Immutable, immutable, architecture, design, csharp, analyzers 20 | true 21 | true 22 | Apex.Analyzers.Immutable.snk 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Semantics/Helper.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | 5 | namespace Apex.Analyzers.Immutable.Semantics 6 | { 7 | internal static class Helper 8 | { 9 | internal static bool HasImmutableAttributeAndShouldVerify(ITypeSymbol type) 10 | { 11 | if(type == null) 12 | { 13 | return false; 14 | } 15 | 16 | var attributes = type.GetAttributes(); 17 | return attributes.Any(x => x.AttributeClass?.Name == "ImmutableAttribute" 18 | && x.AttributeClass?.ContainingNamespace?.Name == "System" 19 | && (x.ConstructorArguments.Length == 0 || (x.ConstructorArguments.First().Value as bool?) == false)); 20 | } 21 | 22 | internal static bool HasImmutableAttribute(ITypeSymbol type) 23 | { 24 | if (type == null) 25 | { 26 | return false; 27 | } 28 | 29 | var attributes = type.GetAttributes(); 30 | return attributes.Any(x => x.AttributeClass?.Name == "ImmutableAttribute" 31 | && x.AttributeClass?.ContainingNamespace?.Name == "System"); 32 | } 33 | 34 | internal static bool IsAutoProperty(IPropertySymbol symbol) 35 | { 36 | var getSyntax = symbol.GetMethod?.DeclaringSyntaxReferences.Select(x => x.GetSyntax()); 37 | var result = getSyntax?.OfType().Where(x => x.Body == null && x.ExpressionBody == null); 38 | if(result != null && result.Any()) 39 | { 40 | return true; 41 | } 42 | 43 | var setSyntax = symbol.SetMethod?.DeclaringSyntaxReferences.Select(x => x.GetSyntax()); 44 | result = setSyntax?.OfType().Where(x => x.Body == null && x.ExpressionBody == null); 45 | return result != null && result.Any(); 46 | } 47 | 48 | 49 | internal static bool HasImmutableNamespace(ITypeSymbol type) 50 | { 51 | return type.ContainingNamespace?.Name == "Immutable" 52 | && type.ContainingNamespace?.ContainingNamespace?.Name == "Collections" 53 | && type.ContainingNamespace?.ContainingNamespace?.ContainingNamespace?.Name == "System"; 54 | } 55 | 56 | internal static bool ShouldCheckMemberTypeForImmutability(ISymbol symbol) 57 | { 58 | return !symbol.IsStatic 59 | && (symbol.DeclaredAccessibility != Accessibility.Private 60 | || !symbol.GetAttributes().Any(x => x.AttributeClass.Name == "NonSerializedAttribute" 61 | && x.AttributeClass.ContainingNamespace?.Name == "System")); 62 | } 63 | 64 | internal static bool IsInitOnlyMethod(IMethodSymbol symbol) 65 | { 66 | return symbol.ReturnTypeCustomModifiers.Any(x => x.Modifier.Name == "IsExternalInit"); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/Rules/IMM003.cs: -------------------------------------------------------------------------------- 1 | using Apex.Analyzers.Immutable.Semantics; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | 5 | namespace Apex.Analyzers.Immutable.Rules 6 | { 7 | internal static class IMM003 8 | { 9 | public const string DiagnosticId = "IMM003"; 10 | 11 | private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.IMM003Title), Resources.ResourceManager, typeof(Resources)); 12 | private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.IMM003MessageFormat), Resources.ResourceManager, typeof(Resources)); 13 | private static readonly LocalizableString MessageFormatGeneric = new LocalizableResourceString(nameof(Resources.IMM003MessageFormatGeneric), Resources.ResourceManager, typeof(Resources)); 14 | 15 | private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.IMM003Description), Resources.ResourceManager, typeof(Resources)); 16 | private const string Category = "Architecture"; 17 | 18 | public static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); 19 | public static DiagnosticDescriptor RuleGeneric = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormatGeneric, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); 20 | 21 | internal static void Initialize(AnalysisContext context, ImmutableTypes immutableTypes) 22 | { 23 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 24 | context.EnableConcurrentExecution(); 25 | context.RegisterSymbolAction(x => AnalyzeSymbol(x, immutableTypes), SymbolKind.Field); 26 | } 27 | 28 | private static void AnalyzeSymbol(SymbolAnalysisContext context, ImmutableTypes immutableTypes) 29 | { 30 | immutableTypes.Initialize(context.Compilation, context.Options, context.CancellationToken); 31 | 32 | var symbol = (IFieldSymbol)context.Symbol; 33 | var containingType = symbol.ContainingType; 34 | if (containingType == null) 35 | { 36 | return; 37 | } 38 | 39 | string genericTypeArgument = null; 40 | 41 | if (Helper.HasImmutableAttributeAndShouldVerify(containingType) 42 | && Helper.ShouldCheckMemberTypeForImmutability(symbol) 43 | && !immutableTypes.IsImmutableType(symbol.Type, ref genericTypeArgument)) 44 | { 45 | if (genericTypeArgument != null) 46 | { 47 | var diagnostic = Diagnostic.Create(RuleGeneric, symbol.Locations[0], symbol.Name, genericTypeArgument); 48 | context.ReportDiagnostic(diagnostic); 49 | } 50 | else 51 | { 52 | var diagnostic = Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name); 53 | context.ReportDiagnostic(diagnostic); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/Rules/IMM004.cs: -------------------------------------------------------------------------------- 1 | using Apex.Analyzers.Immutable.Semantics; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | 5 | namespace Apex.Analyzers.Immutable.Rules 6 | { 7 | internal static class IMM004 8 | { 9 | public const string DiagnosticId = "IMM004"; 10 | 11 | private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.IMM004Title), Resources.ResourceManager, typeof(Resources)); 12 | private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.IMM004MessageFormat), Resources.ResourceManager, typeof(Resources)); 13 | private static readonly LocalizableString MessageFormatGeneric = new LocalizableResourceString(nameof(Resources.IMM004MessageFormatGeneric), Resources.ResourceManager, typeof(Resources)); 14 | 15 | private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.IMM004Description), Resources.ResourceManager, typeof(Resources)); 16 | private const string Category = "Architecture"; 17 | 18 | public static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); 19 | public static DiagnosticDescriptor RuleGeneric = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormatGeneric, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); 20 | 21 | internal static void Initialize(AnalysisContext context, ImmutableTypes immutableTypes) 22 | { 23 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 24 | context.EnableConcurrentExecution(); 25 | context.RegisterSymbolAction(x => AnalyzeSymbol(x, immutableTypes), SymbolKind.Property); 26 | } 27 | 28 | private static void AnalyzeSymbol(SymbolAnalysisContext context, ImmutableTypes immutableTypes) 29 | { 30 | immutableTypes.Initialize(context.Compilation, context.Options, context.CancellationToken); 31 | 32 | var symbol = (IPropertySymbol)context.Symbol; 33 | var containingType = symbol.ContainingType; 34 | if (containingType == null) 35 | { 36 | return; 37 | } 38 | 39 | string genericTypeArgument = null; 40 | if (Helper.HasImmutableAttributeAndShouldVerify(containingType) 41 | && Helper.ShouldCheckMemberTypeForImmutability(symbol) 42 | && !immutableTypes.IsImmutableType(symbol.Type, ref genericTypeArgument) 43 | && Helper.IsAutoProperty(symbol)) 44 | { 45 | if (genericTypeArgument != null) 46 | { 47 | var diagnostic = Diagnostic.Create(RuleGeneric, symbol.Locations[0], symbol.Name, genericTypeArgument); 48 | context.ReportDiagnostic(diagnostic); 49 | } 50 | else 51 | { 52 | var diagnostic = Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name); 53 | context.ReportDiagnostic(diagnostic); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Vsix/Apex.Analyzers.Immutable.Vsix.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14.0 6 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 7 | 8 | 9 | 10 | Debug 11 | AnyCPU 12 | AnyCPU 13 | 2.0 14 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | {EE0B19A1-9F82-402F-BDD5-06B93102602E} 16 | Library 17 | Properties 18 | Apex.Analyzers.Immutable.Vsix 19 | Apex.Analyzers.Immutable 20 | v4.6.1 21 | false 22 | false 23 | false 24 | false 25 | false 26 | false 27 | Roslyn 28 | 29 | 30 | true 31 | full 32 | false 33 | bin\Debug\ 34 | DEBUG;TRACE 35 | prompt 36 | 4 37 | 38 | 39 | pdbonly 40 | true 41 | bin\Release\ 42 | TRACE 43 | prompt 44 | 4 45 | 46 | 47 | Program 48 | $(DevEnvDir)devenv.exe 49 | /rootsuffix Roslyn 50 | 51 | 52 | 53 | Designer 54 | 55 | 56 | 57 | 58 | {35B969E5-F2AA-4B01-BB49-65D682AF9F5A} 59 | Apex.Analyzers.Immutable 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/Rules/IMM005.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Apex.Analyzers.Immutable.Semantics; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | using Microsoft.CodeAnalysis.Operations; 8 | 9 | namespace Apex.Analyzers.Immutable.Rules 10 | { 11 | internal static class IMM005 12 | { 13 | public const string DiagnosticId = "IMM005"; 14 | 15 | private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.IMM005Title), Resources.ResourceManager, typeof(Resources)); 16 | private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.IMM005MessageFormat), Resources.ResourceManager, typeof(Resources)); 17 | 18 | private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.IMM005Description), Resources.ResourceManager, typeof(Resources)); 19 | private const string Category = "Architecture"; 20 | 21 | public static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); 22 | internal static void Initialize(AnalysisContext context) 23 | { 24 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 25 | context.EnableConcurrentExecution(); 26 | context.RegisterOperationAction(AnalyzeOperation, OperationKind.ConstructorBodyOperation); 27 | } 28 | 29 | private static void AnalyzeOperation(OperationAnalysisContext context) 30 | { 31 | if(!Helper.HasImmutableAttributeAndShouldVerify(context.ContainingSymbol?.ContainingType)) 32 | { 33 | return; 34 | } 35 | 36 | CheckOperation(context.Operation, context, false); 37 | } 38 | 39 | private static void CheckOperation(IOperation operation, OperationAnalysisContext context, bool hasReportedThisCapture) 40 | { 41 | if(operation is IInstanceReferenceOperation op 42 | && op.ReferenceKind == InstanceReferenceKind.ContainingTypeInstance 43 | && (op.Parent is IArgumentOperation 44 | || op.Parent is ISymbolInitializerOperation 45 | || op.Parent is IAssignmentOperation)) 46 | { 47 | var diagnostic = Diagnostic.Create(Rule, op.Syntax.GetLocation()); 48 | context.ReportDiagnostic(diagnostic); 49 | } 50 | 51 | bool reportedThisCapture = false; 52 | 53 | if (!hasReportedThisCapture) 54 | { 55 | if (operation.Syntax is AnonymousFunctionExpressionSyntax syntax) 56 | { 57 | var model = operation.SemanticModel; 58 | var dataFlowAnalysis = model.AnalyzeDataFlow(syntax); 59 | var capturedVariables = dataFlowAnalysis.Captured; 60 | if (capturedVariables.Any(x => x is IParameterSymbol p && p.IsThis)) 61 | { 62 | var diagnostic = Diagnostic.Create(Rule, operation.Syntax.GetLocation()); 63 | context.ReportDiagnostic(diagnostic); 64 | reportedThisCapture = true; 65 | } 66 | } 67 | } 68 | 69 | foreach (var child in operation.Children) 70 | { 71 | CheckOperation(child, context, reportedThisCapture || hasReportedThisCapture); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Apex.Analyzers.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28803.352 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Apex.Analyzers.Immutable", "Immutable\Apex.Analyzers.Immutable\Apex.Analyzers.Immutable.csproj", "{35B969E5-F2AA-4B01-BB49-65D682AF9F5A}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Apex.Analyzers.Immutable.Test", "Immutable\Apex.Analyzers.Immutable.Test\Apex.Analyzers.Immutable.Test.csproj", "{4A0F3C3C-6665-425B-9D95-4F3719675F50}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apex.Analyzers.Immutable.Vsix", "Immutable\Apex.Analyzers.Immutable.Vsix\Apex.Analyzers.Immutable.Vsix.csproj", "{EE0B19A1-9F82-402F-BDD5-06B93102602E}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Apex.Analyzers.Immutable.Attributes", "Immutable\Apex.Analyzers.Immutable.Attributes\Apex.Analyzers.Immutable.Attributes.csproj", "{E4AB6278-3D06-4B9A-A3F4-BCBE3321A41F}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C9CB947B-22D0-4339-9AA4-413B7C35D675}" 15 | ProjectSection(SolutionItems) = preProject 16 | azure-pipelines.yml = azure-pipelines.yml 17 | README.md = README.md 18 | EndProjectSection 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Apex.Analyzers.Immutable.Semantics", "Immutable\Apex.Analyzers.Immutable.Semantics\Apex.Analyzers.Immutable.Semantics.csproj", "{C7E447BE-4177-4FB3-8F6F-764FC06E449C}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {35B969E5-F2AA-4B01-BB49-65D682AF9F5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {35B969E5-F2AA-4B01-BB49-65D682AF9F5A}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {35B969E5-F2AA-4B01-BB49-65D682AF9F5A}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {35B969E5-F2AA-4B01-BB49-65D682AF9F5A}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {4A0F3C3C-6665-425B-9D95-4F3719675F50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {4A0F3C3C-6665-425B-9D95-4F3719675F50}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {4A0F3C3C-6665-425B-9D95-4F3719675F50}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {4A0F3C3C-6665-425B-9D95-4F3719675F50}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {EE0B19A1-9F82-402F-BDD5-06B93102602E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {EE0B19A1-9F82-402F-BDD5-06B93102602E}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {EE0B19A1-9F82-402F-BDD5-06B93102602E}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {EE0B19A1-9F82-402F-BDD5-06B93102602E}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {E4AB6278-3D06-4B9A-A3F4-BCBE3321A41F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {E4AB6278-3D06-4B9A-A3F4-BCBE3321A41F}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {E4AB6278-3D06-4B9A-A3F4-BCBE3321A41F}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {E4AB6278-3D06-4B9A-A3F4-BCBE3321A41F}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {C7E447BE-4177-4FB3-8F6F-764FC06E449C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {C7E447BE-4177-4FB3-8F6F-764FC06E449C}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {C7E447BE-4177-4FB3-8F6F-764FC06E449C}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {C7E447BE-4177-4FB3-8F6F-764FC06E449C}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {2BCC1967-9232-4ED7-A17D-4EF54414DF21} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/Rules/IMM008.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Apex.Analyzers.Immutable.Semantics; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | using Microsoft.CodeAnalysis.Operations; 8 | 9 | namespace Apex.Analyzers.Immutable.Rules 10 | { 11 | internal static class IMM008 12 | { 13 | public const string DiagnosticId = "IMM008"; 14 | 15 | private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.IMM008Title), Resources.ResourceManager, typeof(Resources)); 16 | private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.IMM008MessageFormat), Resources.ResourceManager, typeof(Resources)); 17 | 18 | private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.IMM008Description), Resources.ResourceManager, typeof(Resources)); 19 | private const string Category = "Architecture"; 20 | 21 | public static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); 22 | internal static void Initialize(AnalysisContext context) 23 | { 24 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 25 | context.EnableConcurrentExecution(); 26 | // Can't just use MethodBodyOperation in general to analyze property method bodies, but the exceptional 27 | // case is for get only properties, so this should work for init only 28 | // https://github.com/dotnet/roslyn/issues/28163 29 | context.RegisterOperationAction(AnalyzeOperation, OperationKind.MethodBodyOperation); 30 | } 31 | 32 | private static void AnalyzeOperation(OperationAnalysisContext context) 33 | { 34 | if(!Helper.HasImmutableAttributeAndShouldVerify(context.ContainingSymbol?.ContainingType)) 35 | { 36 | return; 37 | } 38 | 39 | if (context.ContainingSymbol is IMethodSymbol method && Helper.IsInitOnlyMethod(method)) 40 | { 41 | CheckOperation(context.Operation, context, false); 42 | } 43 | } 44 | 45 | private static void CheckOperation(IOperation operation, OperationAnalysisContext context, bool hasReportedThisCapture) 46 | { 47 | if(operation is IInstanceReferenceOperation op 48 | && op.ReferenceKind == InstanceReferenceKind.ContainingTypeInstance 49 | && (op.Parent is IArgumentOperation 50 | || op.Parent is ISymbolInitializerOperation 51 | || op.Parent is IAssignmentOperation)) 52 | { 53 | var diagnostic = Diagnostic.Create(Rule, op.Syntax.GetLocation()); 54 | context.ReportDiagnostic(diagnostic); 55 | } 56 | 57 | bool reportedThisCapture = false; 58 | 59 | if (!hasReportedThisCapture) 60 | { 61 | if (operation.Syntax is AnonymousFunctionExpressionSyntax syntax) 62 | { 63 | var model = operation.SemanticModel; 64 | var dataFlowAnalysis = model.AnalyzeDataFlow(syntax); 65 | var capturedVariables = dataFlowAnalysis.Captured; 66 | if (capturedVariables.Any(x => x is IParameterSymbol p && p.IsThis)) 67 | { 68 | var diagnostic = Diagnostic.Create(Rule, operation.Syntax.GetLocation()); 69 | context.ReportDiagnostic(diagnostic); 70 | reportedThisCapture = true; 71 | } 72 | } 73 | } 74 | 75 | foreach (var child in operation.Children) 76 | { 77 | CheckOperation(child, context, reportedThisCapture || hasReportedThisCapture); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/ApexAnalyzersImmutableCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.CodeAnalysis; 7 | using Microsoft.CodeAnalysis.CodeFixes; 8 | using Microsoft.CodeAnalysis.CodeActions; 9 | using Microsoft.CodeAnalysis.CSharp; 10 | using Microsoft.CodeAnalysis.CSharp.Syntax; 11 | using Apex.Analyzers.Immutable.Rules; 12 | 13 | namespace Apex.Analyzers.Immutable 14 | { 15 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ApexAnalyzersImmutableCodeFixProvider)), Shared] 16 | public class ApexAnalyzersImmutableCodeFixProvider : CodeFixProvider 17 | { 18 | private const string titleReadonly = "Make readonly"; 19 | private const string titleSetAccessor = "Remove set accessor"; 20 | 21 | public sealed override ImmutableArray FixableDiagnosticIds 22 | { 23 | get { return ImmutableArray.Create(IMM001.DiagnosticId, IMM002.DiagnosticId); } 24 | } 25 | 26 | public sealed override FixAllProvider GetFixAllProvider() 27 | { 28 | // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers 29 | return WellKnownFixAllProviders.BatchFixer; 30 | } 31 | 32 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 33 | { 34 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 35 | 36 | var diagnostic = context.Diagnostics.First(); 37 | var diagnosticSpan = diagnostic.Location.SourceSpan; 38 | 39 | if (diagnostic.Id == IMM001.DiagnosticId) 40 | { 41 | // Find the field declaration identified by the diagnostic. 42 | var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First(); 43 | 44 | // Register a code action that will invoke the fix. 45 | context.RegisterCodeFix( 46 | CodeAction.Create( 47 | title: titleReadonly, 48 | createChangedDocument: c => AddReadonlyModifierAsync(context.Document, declaration, c), 49 | equivalenceKey: titleReadonly), 50 | diagnostic); 51 | } 52 | else if (diagnostic.Id == IMM002.DiagnosticId) 53 | { 54 | // Find the field declaration identified by the diagnostic. 55 | var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First(); 56 | 57 | // Register a code action that will invoke the fix. 58 | context.RegisterCodeFix( 59 | CodeAction.Create( 60 | title: titleSetAccessor, 61 | createChangedDocument: c => RemoveSetMethodAsync(context.Document, declaration, c), 62 | equivalenceKey: titleSetAccessor), 63 | diagnostic); 64 | } 65 | } 66 | 67 | private async Task AddReadonlyModifierAsync(Document document, FieldDeclarationSyntax decl, CancellationToken cancellationToken) 68 | { 69 | var readonlyToken = SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword); 70 | var newSyntax = decl.WithModifiers(decl.Modifiers.Add(readonlyToken)); 71 | 72 | var oldRoot = await document.GetSyntaxRootAsync(cancellationToken); 73 | var newRoot = oldRoot.ReplaceNode(decl, newSyntax); 74 | 75 | return document.WithSyntaxRoot(newRoot); 76 | } 77 | 78 | private async Task RemoveSetMethodAsync(Document document, PropertyDeclarationSyntax decl, CancellationToken cancellationToken) 79 | { 80 | var setAccessorNode = decl.AccessorList.Accessors.Where(x => x.Keyword.Text == SyntaxFactory.Token(SyntaxKind.SetKeyword).Text) 81 | .FirstOrDefault(); 82 | 83 | if(setAccessorNode == null) 84 | { 85 | return document; 86 | } 87 | 88 | var newSyntax = decl.WithAccessorList(decl.AccessorList.RemoveNode(setAccessorNode, SyntaxRemoveOptions.KeepNoTrivia)); 89 | 90 | var oldRoot = await document.GetSyntaxRootAsync(cancellationToken); 91 | var newRoot = oldRoot.ReplaceNode(decl, newSyntax); 92 | 93 | return document.WithSyntaxRoot(newRoot); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Fields in an immutable type must be readonly 122 | An optional longer localizable description of the diagnostic. 123 | 124 | 125 | Field '{0}' is not declared as readonly 126 | The format-able message the diagnostic displays. 127 | 128 | 129 | Fields in an immutable type must be readonly 130 | The title of the diagnostic. 131 | 132 | 133 | Auto properties in an immutable type must not define a set method 134 | 135 | 136 | Property '{0}' defines a set method 137 | 138 | 139 | Auto properties in an immutable type must not define a set method 140 | 141 | 142 | Types of fields in an immutable type must be immutable 143 | 144 | 145 | Type of field '{0}' is not immutable 146 | 147 | 148 | Type of field '{0}' is not immutable because type argument '{1}' is not immutable 149 | 150 | 151 | Types of fields in an immutable type must be immutable 152 | 153 | 154 | Types of auto properties in an immutable type must be immutable 155 | 156 | 157 | Type of auto property '{0}' is not immutable 158 | 159 | 160 | Type of auto property '{0}' is not immutable because type argument '{1}' is not immutable 161 | 162 | 163 | Types of auto properties in an immutable type must be immutable 164 | 165 | 166 | 'This' should not be passed out of the constructor of an immutable type 167 | 168 | 169 | Possibly incorrect usage of 'this' in the constructor of an immutable type 170 | 171 | 172 | 'This' should not be passed out of the constructor of an immutable type 173 | 174 | 175 | The base type of an immutable type must be 'object' or immutable 176 | 177 | 178 | Type '{0}' base type must be 'object' or immutable 179 | 180 | 181 | The base type of an immutable type must be 'object' or immutable 182 | 183 | 184 | Types derived from an immutable type must be immutable 185 | 186 | 187 | Type '{0}' must be immutable because it derives from '{1}' 188 | 189 | 190 | Types derived from an immutable type must be immutable 191 | 192 | 193 | 'This' should not be passed out of an init only property method of an immutable type 194 | 195 | 196 | Possibly incorrect usage of 'this' in an init only property method of an immutable type 197 | 198 | 199 | 'This' should not be passed out of an init only property method of an immutable type 200 | 201 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Semantics/ImmutableTypes.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Diagnostics; 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Threading; 10 | 11 | namespace Apex.Analyzers.Immutable.Semantics 12 | { 13 | public sealed class ImmutableTypes 14 | { 15 | private readonly ConcurrentDictionary _entries = new ConcurrentDictionary(SymbolEqualityComparer.Default); 16 | 17 | private Compilation Compilation { get; set; } 18 | private AnalyzerOptions AnalyzerOptions { get; set; } 19 | private CancellationToken CancellationToken { get; set; } 20 | 21 | public ImmutableTypes(Compilation compilation, AnalyzerOptions analyzerOptions, CancellationToken cancellationToken) 22 | { 23 | Initialize(compilation, analyzerOptions, cancellationToken); 24 | } 25 | 26 | internal ImmutableTypes() 27 | { 28 | } 29 | 30 | internal void Initialize(Compilation compilation, AnalyzerOptions analyzerOptions, CancellationToken cancellationToken) 31 | { 32 | Compilation = compilation; 33 | AnalyzerOptions = analyzerOptions; 34 | CancellationToken = cancellationToken; 35 | } 36 | 37 | public bool IsImmutableType(ITypeSymbol type, ref string genericTypeArgument) 38 | { 39 | var entry = _entries.GetOrAdd(type, x => GetEntry(x, null)); 40 | if (!string.IsNullOrEmpty(entry.MutableGenericTypeArgument)) 41 | { 42 | genericTypeArgument = entry.MutableGenericTypeArgument; 43 | } 44 | return entry.IsImmutable; 45 | } 46 | 47 | private Entry GetEntry(ITypeSymbol type, HashSet excludedTypes) 48 | { 49 | if (type.TypeKind == TypeKind.Dynamic) 50 | { 51 | return Entry.NotImmutable; 52 | } 53 | 54 | if (type.TypeKind == TypeKind.TypeParameter) 55 | { 56 | return Entry.Immutable; 57 | } 58 | 59 | if (type is INamedTypeSymbol nts && nts.IsGenericType) 60 | { 61 | if (Helper.HasImmutableAttribute(type) || IsWhitelistedType(nts.OriginalDefinition)) 62 | { 63 | if (type.TypeKind == TypeKind.Delegate) 64 | { 65 | return Entry.Immutable; 66 | } 67 | else if (Helper.HasImmutableAttributeAndShouldVerify(type)) 68 | { 69 | return GetGenericImmutableTypeEntry(nts, excludedTypes); 70 | } 71 | else 72 | { 73 | return GetGenericTypeArgumentsEntry(nts, excludedTypes); 74 | } 75 | } 76 | } 77 | 78 | if (Helper.HasImmutableAttribute(type) || IsWhitelistedType(type)) 79 | { 80 | return Entry.Immutable; 81 | } 82 | 83 | return Entry.NotImmutable; 84 | } 85 | 86 | private Entry GetGenericTypeArgumentsEntry(INamedTypeSymbol type, HashSet excludedTypes = null) 87 | { 88 | excludedTypes = excludedTypes ?? new HashSet(SymbolEqualityComparer.Default); 89 | excludedTypes.Add(type); 90 | 91 | var typesToCheck = type.TypeArguments; 92 | return GetGenericTypeArgumentsEntry(typesToCheck, excludedTypes); 93 | } 94 | 95 | private Entry GetGenericImmutableTypeEntry(INamedTypeSymbol type, HashSet excludedTypes = null) 96 | { 97 | excludedTypes = excludedTypes ?? new HashSet(SymbolEqualityComparer.Default); 98 | excludedTypes.Add(type); 99 | 100 | var members = type.GetMembers(); 101 | var fields = members.OfType(); 102 | var autoProperties = members.OfType().Where(x => Helper.IsAutoProperty(x)); 103 | 104 | var filter = ShouldCheckTypeForGenericImmutability(type); 105 | 106 | var typesToCheck = 107 | fields.Select(x => x.Type).Where(filter) 108 | .Concat(autoProperties.Select(x => x.Type).Where(filter)) 109 | .Where(x => !excludedTypes.Contains(x)) 110 | .ToList(); 111 | return GetGenericTypeArgumentsEntry(typesToCheck, excludedTypes); 112 | } 113 | 114 | private Entry GetGenericTypeArgumentsEntry(IEnumerable typesToCheck, HashSet excludedTypes) 115 | { 116 | var result = Entry.Immutable; 117 | foreach (var typeToCheck in typesToCheck) 118 | { 119 | result = GetEntry(typeToCheck, excludedTypes); 120 | if (!result.IsImmutable) 121 | { 122 | if (string.IsNullOrEmpty(result.MutableGenericTypeArgument)) 123 | { 124 | result.MutableGenericTypeArgument = typeToCheck.Name; 125 | } 126 | break; 127 | } 128 | } 129 | return result; 130 | } 131 | 132 | private static Func ShouldCheckTypeForGenericImmutability(INamedTypeSymbol type) 133 | { 134 | return t => 135 | { 136 | if (type.TypeArguments.Any(x => SymbolEqualityComparer.Default.Equals(x, t))) 137 | { 138 | return true; 139 | } 140 | 141 | return t is INamedTypeSymbol nts && nts.IsGenericType; 142 | }; 143 | } 144 | 145 | public bool IsWhitelistedType(ITypeSymbol type) 146 | { 147 | if (Helper.HasImmutableNamespace(type)) 148 | { 149 | return SymbolEqualityComparer.Default.Equals(type, type.OriginalDefinition); 150 | } 151 | 152 | switch (type.SpecialType) 153 | { 154 | case SpecialType.None: 155 | break; 156 | case SpecialType.System_Object: 157 | case SpecialType.System_Enum: 158 | return true; 159 | case SpecialType.System_MulticastDelegate: 160 | case SpecialType.System_Delegate: 161 | break; 162 | case SpecialType.System_ValueType: 163 | return true; 164 | case SpecialType.System_Void: 165 | case SpecialType.System_Boolean: 166 | case SpecialType.System_Char: 167 | case SpecialType.System_SByte: 168 | case SpecialType.System_Byte: 169 | case SpecialType.System_Int16: 170 | case SpecialType.System_UInt16: 171 | case SpecialType.System_Int32: 172 | case SpecialType.System_UInt32: 173 | case SpecialType.System_Int64: 174 | case SpecialType.System_UInt64: 175 | case SpecialType.System_Decimal: 176 | case SpecialType.System_Single: 177 | case SpecialType.System_Double: 178 | case SpecialType.System_String: 179 | return true; 180 | case SpecialType.System_IntPtr: 181 | case SpecialType.System_UIntPtr: 182 | case SpecialType.System_Array: 183 | break; 184 | case SpecialType.System_Collections_IEnumerable: 185 | break; 186 | case SpecialType.System_Collections_Generic_IEnumerable_T: 187 | break; 188 | case SpecialType.System_Collections_Generic_IList_T: 189 | break; 190 | case SpecialType.System_Collections_Generic_ICollection_T: 191 | break; 192 | case SpecialType.System_Collections_IEnumerator: 193 | break; 194 | case SpecialType.System_Collections_Generic_IEnumerator_T: 195 | break; 196 | case SpecialType.System_Collections_Generic_IReadOnlyList_T: 197 | break; 198 | case SpecialType.System_Collections_Generic_IReadOnlyCollection_T: 199 | break; 200 | case SpecialType.System_Nullable_T: 201 | return true; 202 | case SpecialType.System_DateTime: 203 | return true; 204 | case SpecialType.System_Runtime_CompilerServices_IsVolatile: 205 | break; 206 | case SpecialType.System_IDisposable: 207 | break; 208 | case SpecialType.System_TypedReference: 209 | break; 210 | case SpecialType.System_ArgIterator: 211 | break; 212 | case SpecialType.System_RuntimeArgumentHandle: 213 | break; 214 | case SpecialType.System_RuntimeFieldHandle: 215 | break; 216 | case SpecialType.System_RuntimeMethodHandle: 217 | break; 218 | case SpecialType.System_RuntimeTypeHandle: 219 | break; 220 | case SpecialType.System_IAsyncResult: 221 | break; 222 | case SpecialType.System_AsyncCallback: 223 | break; 224 | } 225 | 226 | if (GetWhitelist().Contains(type)) 227 | { 228 | return true; 229 | } 230 | 231 | if (type.BaseType?.SpecialType == SpecialType.System_Enum) 232 | { 233 | return true; 234 | } 235 | 236 | return false; 237 | } 238 | 239 | private const string ImmutableTypesFileName = "ImmutableTypes.txt"; 240 | private IImmutableSet _whitelist; 241 | 242 | private IImmutableSet GetWhitelist() 243 | { 244 | return _whitelist ?? (_whitelist = ReadWhitelist()); 245 | } 246 | 247 | private IImmutableSet ReadWhitelist() 248 | { 249 | var query = 250 | from additionalFile in AnalyzerOptions.AdditionalFiles 251 | where StringComparer.Ordinal.Equals(Path.GetFileName(additionalFile.Path), ImmutableTypesFileName) 252 | let sourceText = additionalFile.GetText(CancellationToken) 253 | where sourceText != null 254 | from line in sourceText.Lines 255 | let text = line.ToString() 256 | where !string.IsNullOrWhiteSpace(text) 257 | select text; 258 | 259 | var entries = query.ToList(); 260 | entries.Add("System.Guid"); 261 | entries.Add("System.TimeSpan"); 262 | entries.Add("System.DateTimeOffset"); 263 | entries.Add("System.Uri"); 264 | entries.Add("System.Nullable`1"); 265 | entries.Add("System.Collections.Generic.KeyValuePair`2"); 266 | var result = new HashSet(SymbolEqualityComparer.Default); 267 | foreach (var entry in entries) 268 | { 269 | var symbols = DocumentationCommentId.GetSymbolsForDeclarationId($"T:{entry}", Compilation); 270 | if (symbols.IsDefaultOrEmpty) 271 | { 272 | continue; 273 | } 274 | foreach (var symbol in symbols) 275 | { 276 | result.Add(symbol); 277 | } 278 | } 279 | return result.ToImmutableHashSet(SymbolEqualityComparer.Default); 280 | } 281 | 282 | private struct Entry 283 | { 284 | public static Entry Immutable => new Entry { IsImmutable = true }; 285 | public static Entry NotImmutable => new Entry { IsImmutable = false }; 286 | 287 | public bool IsImmutable { get; set; } 288 | public string MutableGenericTypeArgument { get; set; } 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Apex.Analyzers.Immutable { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Apex.Analyzers.Immutable.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Fields in an immutable type must be readonly. 65 | /// 66 | internal static string IMM001Description { 67 | get { 68 | return ResourceManager.GetString("IMM001Description", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Field '{0}' is not declared as readonly. 74 | /// 75 | internal static string IMM001MessageFormat { 76 | get { 77 | return ResourceManager.GetString("IMM001MessageFormat", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Fields in an immutable type must be readonly. 83 | /// 84 | internal static string IMM001Title { 85 | get { 86 | return ResourceManager.GetString("IMM001Title", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to Auto properties in an immutable type must not define a set method. 92 | /// 93 | internal static string IMM002Description { 94 | get { 95 | return ResourceManager.GetString("IMM002Description", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to Property '{0}' defines a set method. 101 | /// 102 | internal static string IMM002MessageFormat { 103 | get { 104 | return ResourceManager.GetString("IMM002MessageFormat", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to Auto properties in an immutable type must not define a set method. 110 | /// 111 | internal static string IMM002Title { 112 | get { 113 | return ResourceManager.GetString("IMM002Title", resourceCulture); 114 | } 115 | } 116 | 117 | /// 118 | /// Looks up a localized string similar to Types of fields in an immutable type must be immutable. 119 | /// 120 | internal static string IMM003Description { 121 | get { 122 | return ResourceManager.GetString("IMM003Description", resourceCulture); 123 | } 124 | } 125 | 126 | /// 127 | /// Looks up a localized string similar to Type of field '{0}' is not immutable. 128 | /// 129 | internal static string IMM003MessageFormat { 130 | get { 131 | return ResourceManager.GetString("IMM003MessageFormat", resourceCulture); 132 | } 133 | } 134 | 135 | /// 136 | /// Looks up a localized string similar to Type of field '{0}' is not immutable because type argument '{1}' is not immutable. 137 | /// 138 | internal static string IMM003MessageFormatGeneric { 139 | get { 140 | return ResourceManager.GetString("IMM003MessageFormatGeneric", resourceCulture); 141 | } 142 | } 143 | 144 | /// 145 | /// Looks up a localized string similar to Types of fields in an immutable type must be immutable. 146 | /// 147 | internal static string IMM003Title { 148 | get { 149 | return ResourceManager.GetString("IMM003Title", resourceCulture); 150 | } 151 | } 152 | 153 | /// 154 | /// Looks up a localized string similar to Types of auto properties in an immutable type must be immutable. 155 | /// 156 | internal static string IMM004Description { 157 | get { 158 | return ResourceManager.GetString("IMM004Description", resourceCulture); 159 | } 160 | } 161 | 162 | /// 163 | /// Looks up a localized string similar to Type of auto property '{0}' is not immutable. 164 | /// 165 | internal static string IMM004MessageFormat { 166 | get { 167 | return ResourceManager.GetString("IMM004MessageFormat", resourceCulture); 168 | } 169 | } 170 | 171 | /// 172 | /// Looks up a localized string similar to Type of auto property '{0}' is not immutable because type argument '{1}' is not immutable. 173 | /// 174 | internal static string IMM004MessageFormatGeneric { 175 | get { 176 | return ResourceManager.GetString("IMM004MessageFormatGeneric", resourceCulture); 177 | } 178 | } 179 | 180 | /// 181 | /// Looks up a localized string similar to Types of auto properties in an immutable type must be immutable. 182 | /// 183 | internal static string IMM004Title { 184 | get { 185 | return ResourceManager.GetString("IMM004Title", resourceCulture); 186 | } 187 | } 188 | 189 | /// 190 | /// Looks up a localized string similar to 'This' should not be passed out of the constructor of an immutable type. 191 | /// 192 | internal static string IMM005Description { 193 | get { 194 | return ResourceManager.GetString("IMM005Description", resourceCulture); 195 | } 196 | } 197 | 198 | /// 199 | /// Looks up a localized string similar to Possibly incorrect usage of 'this' in the constructor of an immutable type. 200 | /// 201 | internal static string IMM005MessageFormat { 202 | get { 203 | return ResourceManager.GetString("IMM005MessageFormat", resourceCulture); 204 | } 205 | } 206 | 207 | /// 208 | /// Looks up a localized string similar to 'This' should not be passed out of the constructor of an immutable type. 209 | /// 210 | internal static string IMM005Title { 211 | get { 212 | return ResourceManager.GetString("IMM005Title", resourceCulture); 213 | } 214 | } 215 | 216 | /// 217 | /// Looks up a localized string similar to The base type of an immutable type must be 'object' or immutable. 218 | /// 219 | internal static string IMM006Description { 220 | get { 221 | return ResourceManager.GetString("IMM006Description", resourceCulture); 222 | } 223 | } 224 | 225 | /// 226 | /// Looks up a localized string similar to Type '{0}' base type must be 'object' or immutable. 227 | /// 228 | internal static string IMM006MessageFormat { 229 | get { 230 | return ResourceManager.GetString("IMM006MessageFormat", resourceCulture); 231 | } 232 | } 233 | 234 | /// 235 | /// Looks up a localized string similar to The base type of an immutable type must be 'object' or immutable. 236 | /// 237 | internal static string IMM006Title { 238 | get { 239 | return ResourceManager.GetString("IMM006Title", resourceCulture); 240 | } 241 | } 242 | 243 | /// 244 | /// Looks up a localized string similar to Types derived from an immutable type must be immutable. 245 | /// 246 | internal static string IMM007Description { 247 | get { 248 | return ResourceManager.GetString("IMM007Description", resourceCulture); 249 | } 250 | } 251 | 252 | /// 253 | /// Looks up a localized string similar to Type '{0}' must be immutable because it derives from '{1}'. 254 | /// 255 | internal static string IMM007MessageFormat { 256 | get { 257 | return ResourceManager.GetString("IMM007MessageFormat", resourceCulture); 258 | } 259 | } 260 | 261 | /// 262 | /// Looks up a localized string similar to Types derived from an immutable type must be immutable. 263 | /// 264 | internal static string IMM007Title { 265 | get { 266 | return ResourceManager.GetString("IMM007Title", resourceCulture); 267 | } 268 | } 269 | 270 | /// 271 | /// Looks up a localized string similar to 'This' should not be passed out of an init only property method of an immutable type. 272 | /// 273 | internal static string IMM008Description { 274 | get { 275 | return ResourceManager.GetString("IMM008Description", resourceCulture); 276 | } 277 | } 278 | 279 | /// 280 | /// Looks up a localized string similar to Possibly incorrect usage of 'this' in an init only property method of an immutable type. 281 | /// 282 | internal static string IMM008MessageFormat { 283 | get { 284 | return ResourceManager.GetString("IMM008MessageFormat", resourceCulture); 285 | } 286 | } 287 | 288 | /// 289 | /// Looks up a localized string similar to 'This' should not be passed out of an init only property method of an immutable type. 290 | /// 291 | internal static string IMM008Title { 292 | get { 293 | return ResourceManager.GetString("IMM008Title", resourceCulture); 294 | } 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Test/Verifiers/CSharpCodeFixVerifier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Threading; 8 | using Microsoft.CodeAnalysis; 9 | using Microsoft.CodeAnalysis.CodeActions; 10 | using Microsoft.CodeAnalysis.CodeFixes; 11 | using Microsoft.CodeAnalysis.CSharp; 12 | using Microsoft.CodeAnalysis.Diagnostics; 13 | using Microsoft.CodeAnalysis.Formatting; 14 | using Microsoft.CodeAnalysis.Simplification; 15 | using Microsoft.CodeAnalysis.Text; 16 | using Xunit; 17 | 18 | namespace TestHelper 19 | { 20 | public static class CSharpCodeFixVerifier 21 | where TAnalyzer : DiagnosticAnalyzer, new() 22 | where TCodeFix : CodeFixProvider, new() 23 | { 24 | public class Test 25 | { 26 | private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); 27 | private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location); 28 | private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location); 29 | private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location); 30 | private static readonly MetadataReference AttributeReference = MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location); 31 | private static readonly MetadataReference AnalyzerReference = MetadataReference.CreateFromFile(typeof(ImmutableAttribute).Assembly.Location); 32 | private static readonly MetadataReference ImmutableReference = MetadataReference.CreateFromFile(typeof(ImmutableArray).Assembly.Location); 33 | private static readonly MetadataReference NetStandard2Reference = MetadataReference.CreateFromFile(Assembly.Load("netstandard, Version=2.0.0.0").Location); 34 | 35 | private static readonly string TestFileName = "Test0.cs"; 36 | private static readonly string TestProjectName = "TestProject"; 37 | 38 | public string TestCode { get; internal set; } 39 | public string FixedCode { get; internal set; } 40 | public List MetadataReferences { get; } = new List(); 41 | public List AdditionalFiles { get; } = new List(); 42 | public List ExpectedDiagnostics { get; } = new List(); 43 | 44 | public Test() 45 | { 46 | MetadataReferences.AddRange(new[] 47 | { 48 | CorlibReference, 49 | SystemCoreReference, 50 | CSharpSymbolsReference, 51 | CodeAnalysisReference, 52 | AnalyzerReference, 53 | AttributeReference, 54 | ImmutableReference, 55 | NetStandard2Reference 56 | }); 57 | } 58 | public void Run() 59 | { 60 | var analyzer = new TAnalyzer(); 61 | var fix = new TCodeFix(); 62 | var actualResults = GetSortedDiagnostics(analyzer); 63 | VerifyDiagnosticResults(actualResults, analyzer, ExpectedDiagnostics.ToArray()); 64 | 65 | if(FixedCode != null) 66 | { 67 | VerifyFix(analyzer, fix, TestCode, FixedCode); 68 | } 69 | } 70 | 71 | #region Get Diagnostics 72 | 73 | /// 74 | /// Given classes in the form of strings, their language, and an IDiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document. 75 | /// 76 | /// The analyzer to be run on the sources 77 | /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location 78 | private Diagnostic[] GetSortedDiagnostics(TAnalyzer analyzer) 79 | { 80 | return GetSortedDiagnosticsFromDocument(analyzer, GetDocument()); 81 | } 82 | 83 | private Diagnostic[] GetSortedDiagnosticsFromDocument(TAnalyzer analyzer, Document document) 84 | { 85 | var diagnostics = new List(); 86 | var compilation = document.Project.GetCompilationAsync().Result; 87 | var options = new AnalyzerOptions(AdditionalFiles.ToImmutableArray()); 88 | var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer), options); 89 | var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result; 90 | diagnostics.AddRange(diags.Concat(compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error))); 91 | var results = SortDiagnostics(diagnostics); 92 | diagnostics.Clear(); 93 | return results; 94 | } 95 | 96 | private static Diagnostic[] SortDiagnostics(IEnumerable diagnostics) 97 | { 98 | return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 99 | } 100 | 101 | #endregion 102 | 103 | #region Set up compilation and documents 104 | private Document GetDocument() 105 | { 106 | var project = CreateProject(); 107 | return project.Documents.First(); 108 | 109 | Project CreateProject() 110 | { 111 | var projectId = ProjectId.CreateNewId(debugName: TestProjectName); 112 | 113 | var solution = new AdhocWorkspace() 114 | .CurrentSolution 115 | .AddProject(projectId, TestProjectName, TestProjectName, LanguageNames.CSharp) 116 | .AddMetadataReferences(projectId, MetadataReferences) 117 | .WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.ConsoleApplication, allowUnsafe: true)); 118 | 119 | var documentId = DocumentId.CreateNewId(projectId, debugName: TestFileName); 120 | var documentIdForInit = DocumentId.CreateNewId(projectId, debugName: "IsInitOnly.cs"); 121 | solution = solution.AddDocument(documentId, TestFileName, SourceText.From(TestCode)) 122 | .AddDocument(documentIdForInit, "IsInitOnly.cs", SourceText.From(@"namespace System.Runtime.CompilerServices 123 | { 124 | public sealed class IsExternalInit 125 | { 126 | } 127 | }")); 128 | return solution.GetProject(projectId); 129 | } 130 | } 131 | #endregion 132 | 133 | #region Actual comparisons and verifications 134 | /// 135 | /// General verifier for codefixes. 136 | /// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes. 137 | /// Then gets the string after the codefix is applied and compares it with the expected result. 138 | /// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true. 139 | /// 140 | /// The analyzer to be applied to the source code 141 | /// The codefix to be applied to the code wherever the relevant Diagnostic is found 142 | /// A class in the form of a string before the CodeFix was applied to it 143 | /// A class in the form of a string after the CodeFix was applied to it 144 | private void VerifyFix(TAnalyzer analyzer, TCodeFix codeFixProvider, string oldSource, string newSource) 145 | { 146 | var document = GetDocument(); 147 | var analyzerDiagnostics = GetSortedDiagnosticsFromDocument(analyzer, document); 148 | var compilerDiagnostics = GetCompilerDiagnostics(document); 149 | var attempts = analyzerDiagnostics.Length; 150 | 151 | for (int i = 0; i < attempts; ++i) 152 | { 153 | var actions = new List(); 154 | var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None); 155 | codeFixProvider.RegisterCodeFixesAsync(context).Wait(); 156 | 157 | if (!actions.Any()) 158 | { 159 | break; 160 | } 161 | 162 | document = ApplyFix(document, actions.ElementAt(0)); 163 | analyzerDiagnostics = GetSortedDiagnosticsFromDocument(analyzer, document); 164 | 165 | //check if there are analyzer diagnostics left after the code fix 166 | if (!analyzerDiagnostics.Any()) 167 | { 168 | break; 169 | } 170 | } 171 | 172 | //after applying all of the code fixes, compare the resulting string to the inputted one 173 | var actual = GetStringFromDocument(document); 174 | Assert.Equal(newSource, actual); 175 | 176 | Document ApplyFix(Document document, CodeAction codeAction) 177 | { 178 | var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result; 179 | var solution = operations.OfType().Single().ChangedSolution; 180 | return solution.GetDocument(document.Id); 181 | } 182 | 183 | string GetStringFromDocument(Document document) 184 | { 185 | var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result; 186 | var root = simplifiedDoc.GetSyntaxRootAsync().Result; 187 | root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace); 188 | return root.GetText().ToString(); 189 | } 190 | 191 | IEnumerable GetCompilerDiagnostics(Document document) 192 | { 193 | return document.GetSemanticModelAsync().Result.GetDiagnostics(); 194 | } 195 | } 196 | 197 | /// 198 | /// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results. 199 | /// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic. 200 | /// 201 | /// The Diagnostics found by the compiler after running the analyzer on the source code 202 | /// The analyzer that was being run on the sources 203 | /// Diagnostic Results that should have appeared in the code 204 | private static void VerifyDiagnosticResults(IEnumerable actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults) 205 | { 206 | int expectedCount = expectedResults.Count(); 207 | int actualCount = actualResults.Count(); 208 | 209 | if (expectedCount != actualCount) 210 | { 211 | string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE."; 212 | 213 | Assert.True(false, 214 | string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput)); 215 | } 216 | 217 | for (int i = 0; i < expectedResults.Length; i++) 218 | { 219 | var actual = actualResults.ElementAt(i); 220 | var expected = expectedResults[i]; 221 | 222 | if (expected.Line == -1 && expected.Column == -1) 223 | { 224 | if (actual.Location != Location.None) 225 | { 226 | Assert.True(false, 227 | string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}", 228 | FormatDiagnostics(analyzer, actual))); 229 | } 230 | } 231 | else 232 | { 233 | VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First()); 234 | var additionalLocations = actual.AdditionalLocations.ToArray(); 235 | 236 | if (additionalLocations.Length != expected.Locations.Length - 1) 237 | { 238 | Assert.True(false, 239 | string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n", 240 | expected.Locations.Length - 1, additionalLocations.Length, 241 | FormatDiagnostics(analyzer, actual))); 242 | } 243 | 244 | for (int j = 0; j < additionalLocations.Length; ++j) 245 | { 246 | VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]); 247 | } 248 | } 249 | 250 | if (actual.Id != expected.Id) 251 | { 252 | Assert.True(false, 253 | string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", 254 | expected.Id, actual.Id, FormatDiagnostics(analyzer, actual))); 255 | } 256 | 257 | if (actual.Severity != expected.Severity) 258 | { 259 | Assert.True(false, 260 | string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", 261 | expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual))); 262 | } 263 | 264 | if (actual.GetMessage() != expected.Message) 265 | { 266 | Assert.True(false, 267 | string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", 268 | expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual))); 269 | } 270 | } 271 | } 272 | 273 | /// 274 | /// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult. 275 | /// 276 | /// The analyzer that was being run on the sources 277 | /// The diagnostic that was found in the code 278 | /// The Location of the Diagnostic found in the code 279 | /// The DiagnosticResultLocation that should have been found 280 | private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected) 281 | { 282 | var actualSpan = actual.GetLineSpan(); 283 | 284 | Assert.True(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")), 285 | string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", 286 | expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic))); 287 | 288 | var actualLinePosition = actualSpan.StartLinePosition; 289 | 290 | // Only check line position if there is an actual line in the real diagnostic 291 | if (actualLinePosition.Line > 0) 292 | { 293 | if (actualLinePosition.Line + 1 != expected.Line) 294 | { 295 | Assert.True(false, 296 | string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", 297 | expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic))); 298 | } 299 | } 300 | 301 | // Only check column position if there is an actual column position in the real diagnostic 302 | if (actualLinePosition.Character > 0) 303 | { 304 | if (actualLinePosition.Character + 1 != expected.Column) 305 | { 306 | Assert.True(false, 307 | string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", 308 | expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic))); 309 | } 310 | } 311 | } 312 | #endregion 313 | 314 | #region Formatting Diagnostics 315 | /// 316 | /// Helper method to format a Diagnostic into an easily readable string 317 | /// 318 | /// The analyzer that this verifier tests 319 | /// The Diagnostics to be formatted 320 | /// The Diagnostics formatted as a string 321 | private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics) 322 | { 323 | var builder = new StringBuilder(); 324 | for (int i = 0; i < diagnostics.Length; ++i) 325 | { 326 | builder.AppendLine("// " + diagnostics[i].ToString()); 327 | 328 | var analyzerType = analyzer.GetType(); 329 | var rules = analyzer.SupportedDiagnostics; 330 | 331 | foreach (var rule in rules) 332 | { 333 | if (rule != null && rule.Id == diagnostics[i].Id) 334 | { 335 | var location = diagnostics[i].Location; 336 | if (location == Location.None) 337 | { 338 | builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id); 339 | } 340 | else 341 | { 342 | Assert.True(location.IsInSource, 343 | $"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n"); 344 | 345 | string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt"; 346 | var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition; 347 | 348 | builder.AppendFormat("{0}({1}, {2}, {3}.{4})", 349 | resultMethodName, 350 | linePosition.Line + 1, 351 | linePosition.Character + 1, 352 | analyzerType.Name, 353 | rule.Id); 354 | } 355 | 356 | if (i != diagnostics.Length - 1) 357 | { 358 | builder.Append(','); 359 | } 360 | 361 | builder.AppendLine(); 362 | break; 363 | } 364 | } 365 | } 366 | return builder.ToString(); 367 | } 368 | #endregion 369 | } 370 | 371 | public static void VerifyAnalyzer(string source, params DiagnosticResult[] expected) 372 | { 373 | var test = new Test 374 | { 375 | TestCode = source, 376 | }; 377 | test.ExpectedDiagnostics.AddRange(expected); 378 | test.Run(); 379 | } 380 | 381 | public static void VerifyCodeFix(string source, DiagnosticResult expected, string fixedSource) 382 | => VerifyCodeFix(source, new[] { expected }, fixedSource); 383 | 384 | public static void VerifyCodeFix(string source, DiagnosticResult[] expected, string fixedSource) 385 | { 386 | var test = new Test 387 | { 388 | TestCode = source, 389 | FixedCode = fixedSource, 390 | }; 391 | 392 | test.ExpectedDiagnostics.AddRange(expected); 393 | test.Run(); 394 | } 395 | } 396 | } -------------------------------------------------------------------------------- /Immutable/Apex.Analyzers.Immutable.Test/ApexAnalyzersImmutableUnitTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CodeFixes; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | using System; 5 | using TestHelper; 6 | using Apex.Analyzers.Immutable; 7 | using Xunit; 8 | 9 | namespace Apex.Analyzers.Immutable.Test 10 | { 11 | public class UnitTest 12 | { 13 | 14 | //No diagnostics expected to show up 15 | [Fact] 16 | public void Empty() 17 | { 18 | var test = @"namespace Test { public class Program { public static void Main() {} }}"; 19 | 20 | VerifyCSharpDiagnostic(test); 21 | } 22 | 23 | [Fact] 24 | public void IMM001MemberFieldNotReadonly() 25 | { 26 | var test = GetCode(@" 27 | [Immutable] 28 | class Test 29 | { 30 | private int x; 31 | } 32 | "); 33 | var expected = new DiagnosticResult 34 | { 35 | Id = "IMM001", 36 | Message = "Field 'x' is not declared as readonly", 37 | Severity = DiagnosticSeverity.Error, 38 | Locations = 39 | new[] { 40 | new DiagnosticResultLocation("Test0.cs", 16, 25) 41 | } 42 | }; 43 | 44 | var fixtest = test.Replace("private int x", "private readonly int x"); 45 | 46 | VerifyCSharpFix(test, new[] { expected }, fixtest); 47 | } 48 | 49 | [Fact] 50 | public void IMM001MemberFieldNotReadonlyNonSerialized() 51 | { 52 | var test = GetCode(@" 53 | [Immutable] 54 | class Test 55 | { 56 | [NonSerialized] 57 | private int x; 58 | } 59 | "); 60 | VerifyCSharpDiagnostic(test); 61 | } 62 | 63 | [Fact] 64 | public void IMM001MemberFieldNotReadonlyNonSerializedPublic() 65 | { 66 | var test = GetCode(@" 67 | [Immutable] 68 | class Test 69 | { 70 | [NonSerialized] 71 | public int x; 72 | } 73 | "); 74 | 75 | var expected = new DiagnosticResult 76 | { 77 | Id = "IMM001", 78 | Message = "Field 'x' is not declared as readonly", 79 | Severity = DiagnosticSeverity.Error, 80 | Locations = 81 | new[] { 82 | new DiagnosticResultLocation("Test0.cs", 17, 24) 83 | } 84 | }; 85 | 86 | var fixtest = test.Replace("public int x", "public readonly int x"); 87 | VerifyCSharpFix(test, new[] { expected }, fixtest); 88 | } 89 | 90 | [Fact] 91 | public void IMM001MemberFieldReadonly() 92 | { 93 | var test = GetCode(@" 94 | [Immutable] 95 | class Test 96 | { 97 | private readonly int x; 98 | } 99 | "); 100 | VerifyCSharpDiagnostic(test); 101 | } 102 | 103 | [Fact] 104 | public void IMM001StaticFieldNotReadonly() 105 | { 106 | var test = GetCode(@" 107 | [Immutable] 108 | class Test 109 | { 110 | private static int x; 111 | } 112 | "); 113 | VerifyCSharpDiagnostic(test); 114 | } 115 | 116 | [Fact] 117 | public void IMM001ConstField() 118 | { 119 | var test = GetCode(@" 120 | [Immutable] 121 | class Test 122 | { 123 | private const int x = 1; 124 | } 125 | "); 126 | VerifyCSharpDiagnostic(test); 127 | } 128 | 129 | [Fact] 130 | public void IMM002MemberPropNotReadonly() 131 | { 132 | var test = GetCode(@" 133 | [Immutable] 134 | class Test 135 | { 136 | private int x {get; set;} 137 | } 138 | "); 139 | var expected = new DiagnosticResult 140 | { 141 | Id = "IMM002", 142 | Message = "Property 'x' defines a set method", 143 | Severity = DiagnosticSeverity.Error, 144 | Locations = 145 | new[] { 146 | new DiagnosticResultLocation("Test0.cs", 16, 25) 147 | } 148 | }; 149 | 150 | var fixtest = test.Replace("private int x {get; set;}", "private int x {get; }"); 151 | VerifyCSharpFix(test, new[] { expected }, fixtest); 152 | } 153 | 154 | [Fact] 155 | public void IMM002MemberPropNotReadonlyNotAuto() 156 | { 157 | var test = GetCode(@" 158 | [Immutable] 159 | class Test 160 | { 161 | private int x {get => 1; set {} } 162 | } 163 | "); 164 | VerifyCSharpDiagnostic(test); 165 | } 166 | 167 | [Fact] 168 | public void IMM002MemberPropSetPassingOutInstanceReference() 169 | { 170 | var test = GetCode(@" 171 | [Immutable] 172 | class Test 173 | { 174 | private readonly int x; 175 | private int X {get => x; set { M(this); } } 176 | 177 | private static int M(Test t) { 178 | return t.X + 1; 179 | } 180 | } 181 | "); 182 | VerifyCSharpDiagnostic(test); 183 | } 184 | 185 | [Fact] 186 | public void IMM002MemberPropReadonly() 187 | { 188 | var test = GetCode(@" 189 | [Immutable] 190 | class Test 191 | { 192 | private int x {get;} 193 | } 194 | "); 195 | VerifyCSharpDiagnostic(test); 196 | } 197 | 198 | [Fact] 199 | public void IMM002MemberPropReadonlyInit() 200 | { 201 | var test = GetCode(@" 202 | [Immutable] 203 | class Test 204 | { 205 | private int x {get; init;} 206 | } 207 | "); 208 | VerifyCSharpDiagnostic(test); 209 | } 210 | 211 | [Fact] 212 | public void IMM002StaticPropNotReadonly() 213 | { 214 | var test = GetCode(@" 215 | [Immutable] 216 | class Test 217 | { 218 | private static int x {get; set;} 219 | } 220 | "); 221 | VerifyCSharpDiagnostic(test); 222 | } 223 | 224 | [Fact] 225 | public void IMM003MemberFieldsWhitelistedByConvention() 226 | { 227 | var test = GetCode(@" 228 | enum TestEnum { 229 | A 230 | } 231 | [Immutable] 232 | class Test 233 | { 234 | private readonly TestEnum x; 235 | private readonly byte a; 236 | private readonly char b; 237 | private readonly sbyte c; 238 | private readonly short d; 239 | private readonly ushort e; 240 | private readonly int f; 241 | private readonly uint g; 242 | private readonly long h; 243 | private readonly ulong i; 244 | private readonly string j; 245 | private readonly DateTime k; 246 | private readonly float l; 247 | private readonly double m; 248 | private readonly decimal n; 249 | private readonly object o; 250 | private readonly Guid p; 251 | private readonly TimeSpan q; 252 | private readonly DateTimeOffset r; 253 | private readonly int? s; 254 | private readonly KeyValuePair t; 255 | } 256 | "); 257 | VerifyCSharpDiagnostic(test); 258 | } 259 | 260 | [Fact] 261 | public void IMM003MemberFieldsWhitelistedByConfiguration() 262 | { 263 | var code = GetCode(@" 264 | [Immutable] 265 | class Test 266 | { 267 | private readonly Func a; 268 | } 269 | "); 270 | 271 | string whitelist = $"System.Func`1"; 272 | var test = new CSharpCodeFixVerifier.Test(); 273 | test.TestCode = code; 274 | test.AdditionalFiles.Add(new AdditionalFile("ImmutableTypes.txt", whitelist)); 275 | test.Run(); 276 | } 277 | 278 | [Fact] 279 | public void IMM003ImmutableDelegatesDoNotCheckTypeParametersForImmutability() 280 | { 281 | var code = GetCode(@" 282 | class NotImmutable {} 283 | 284 | [Immutable] 285 | class Test 286 | { 287 | private readonly Func a; 288 | } 289 | "); 290 | string whitelist = $"System.Func`1"; 291 | var test = new CSharpCodeFixVerifier.Test(); 292 | test.TestCode = code; 293 | test.AdditionalFiles.Add(new AdditionalFile("ImmutableTypes.txt", whitelist)); 294 | test.Run(); 295 | } 296 | 297 | [Fact] 298 | public void IMM003MemberFieldsImmutable() 299 | { 300 | var test = GetCode(@" 301 | [Immutable] 302 | class TestI { 303 | } 304 | [Immutable] 305 | class Test 306 | { 307 | private readonly TestI x; 308 | } 309 | "); 310 | VerifyCSharpDiagnostic(test); 311 | } 312 | 313 | [Fact] 314 | public void IMM003MemberFieldsImmutableNamespace() 315 | { 316 | var test = GetCode(@" 317 | [Immutable] 318 | class Test 319 | { 320 | private readonly ImmutableArray x; 321 | } 322 | "); 323 | VerifyCSharpDiagnostic(test); 324 | } 325 | 326 | [Fact] 327 | public void IMM003MemberFieldsGeneric() 328 | { 329 | var test = GetCode(@" 330 | [Immutable] 331 | class Test 332 | { 333 | private readonly T x; 334 | } 335 | "); 336 | VerifyCSharpDiagnostic(test); 337 | } 338 | 339 | [Fact] 340 | public void IMM003MemberFieldsGenericNotImmutableConcrete() 341 | { 342 | var test = GetCode(@" 343 | public class MutableClass 344 | { 345 | } 346 | 347 | [Immutable] 348 | public class Class1 349 | { 350 | private readonly int x; 351 | private readonly T Value; 352 | } 353 | 354 | [Immutable] 355 | public class Test 356 | { 357 | private readonly Class1 TestValue; 358 | } 359 | "); 360 | var expected = new DiagnosticResult 361 | { 362 | Id = "IMM003", 363 | Message = "Type of field 'TestValue' is not immutable because type argument 'MutableClass' is not immutable", 364 | Severity = DiagnosticSeverity.Error, 365 | Locations = 366 | new[] { 367 | new DiagnosticResultLocation("Test0.cs", 27, 47) 368 | } 369 | }; 370 | 371 | VerifyCSharpDiagnostic(test, expected); 372 | } 373 | 374 | [Fact] 375 | public void IMM003MemberFieldsNotImmutableNestedInGenericOnFaith_should_not_validate_non_type_arguments() 376 | { 377 | var test = GetCode(@" 378 | [Immutable(onFaith: true)] 379 | public class Class1 380 | { 381 | public class MutableClass 382 | { 383 | } 384 | 385 | private readonly int x; 386 | private readonly MutableClass Value; 387 | } 388 | 389 | [Immutable] 390 | public class Test 391 | { 392 | private readonly Class1 TestValue; 393 | } 394 | "); 395 | VerifyCSharpDiagnostic(test); 396 | } 397 | 398 | [Fact] 399 | public void IMM003MemberFieldsNotImmutableNestedInGenericOnFaith_should_validate_type_arguments() 400 | { 401 | var test = GetCode(@" 402 | public class MutableClass 403 | { 404 | } 405 | 406 | [Immutable(onFaith: true)] 407 | public class Class1 408 | { 409 | private readonly int x; 410 | private readonly T Value; 411 | } 412 | 413 | [Immutable] 414 | public class Test 415 | { 416 | private readonly Class1 TestValue; 417 | } 418 | "); 419 | var expected = new DiagnosticResult 420 | { 421 | Id = "IMM003", 422 | Message = "Type of field 'TestValue' is not immutable because type argument 'MutableClass' is not immutable", 423 | Severity = DiagnosticSeverity.Error, 424 | Locations = 425 | new[] { 426 | new DiagnosticResultLocation("Test0.cs", 27, 47) 427 | } 428 | }; 429 | 430 | VerifyCSharpDiagnostic(test, expected); 431 | } 432 | 433 | [Fact] 434 | public void IMM003MemberFieldsNestedGenericNotImmutableConcrete() 435 | { 436 | var test = GetCode(@" 437 | public class MutableClass 438 | { 439 | } 440 | 441 | [Immutable] 442 | public class Class1 443 | { 444 | private readonly int x; 445 | private readonly T Value; 446 | } 447 | 448 | [Immutable] 449 | public class Test 450 | { 451 | private readonly Class1> TestValue; 452 | } 453 | "); 454 | var expected = new DiagnosticResult 455 | { 456 | Id = "IMM003", 457 | Message = "Type of field 'TestValue' is not immutable because type argument 'MutableClass' is not immutable", 458 | Severity = DiagnosticSeverity.Error, 459 | Locations = 460 | new[] { 461 | new DiagnosticResultLocation("Test0.cs", 27, 55) 462 | } 463 | }; 464 | 465 | VerifyCSharpDiagnostic(test, expected); 466 | } 467 | 468 | [Fact] 469 | public void IMM003MemberFieldsGenericFromSystemNotImmutableConcrete() 470 | { 471 | var test = GetCode(@" 472 | public class MutableClass 473 | { 474 | } 475 | 476 | [Immutable] 477 | public class Test 478 | { 479 | private readonly ImmutableSortedDictionary TestValue; 480 | } 481 | "); 482 | var expected = new DiagnosticResult 483 | { 484 | Id = "IMM003", 485 | Message = "Type of field 'TestValue' is not immutable because type argument 'MutableClass' is not immutable", 486 | Severity = DiagnosticSeverity.Error, 487 | Locations = 488 | new[] { 489 | new DiagnosticResultLocation("Test0.cs", 20, 71) 490 | } 491 | }; 492 | 493 | VerifyCSharpDiagnostic(test, expected); 494 | } 495 | 496 | [Fact] 497 | public void IMM003MemberFieldsGenericNotImmutableConcretePropogation() 498 | { 499 | var test = GetCode(@" 500 | public class MutableClass 501 | { 502 | } 503 | 504 | [Immutable] 505 | public class ImmutableTuple 506 | { 507 | public readonly T Value; 508 | } 509 | 510 | [Immutable] 511 | public class Class1 512 | { 513 | private readonly int x; 514 | private readonly ImmutableTuple Value; 515 | } 516 | 517 | [Immutable] 518 | public class Test 519 | { 520 | private readonly Class1 TestValue; 521 | } 522 | "); 523 | var expected = new DiagnosticResult 524 | { 525 | Id = "IMM003", 526 | Message = "Type of field 'TestValue' is not immutable because type argument 'MutableClass' is not immutable", 527 | Severity = DiagnosticSeverity.Error, 528 | Locations = 529 | new[] { 530 | new DiagnosticResultLocation("Test0.cs", 33, 47) 531 | } 532 | }; 533 | 534 | VerifyCSharpDiagnostic(test, expected); 535 | } 536 | 537 | [Fact] 538 | public void IMM003MemberFieldsGenericNotImmutableConcretePropogationLoop() 539 | { 540 | var test = GetCode(@" 541 | public class MutableClass 542 | { 543 | } 544 | 545 | [Immutable] 546 | public class ImmutableTuple 547 | { 548 | public readonly T Value; 549 | } 550 | 551 | [Immutable] 552 | public class Class2 553 | { 554 | private readonly int x; 555 | private readonly ImmutableTuple> Value; 556 | private readonly ImmutableTuple Value2; 557 | } 558 | 559 | [Immutable] 560 | public class Class1 561 | { 562 | private readonly int x; 563 | private readonly ImmutableTuple> Value; 564 | } 565 | 566 | [Immutable] 567 | public class Test 568 | { 569 | private readonly Class1 TestValue; 570 | } 571 | "); 572 | var expected = new DiagnosticResult 573 | { 574 | Id = "IMM003", 575 | Message = "Type of field 'TestValue' is not immutable because type argument 'MutableClass' is not immutable", 576 | Severity = DiagnosticSeverity.Error, 577 | Locations = 578 | new[] { 579 | new DiagnosticResultLocation("Test0.cs", 41, 47) 580 | } 581 | }; 582 | 583 | VerifyCSharpDiagnostic(test, expected); 584 | } 585 | 586 | [Fact] 587 | public void IMM003MemberFieldsNotImmutable() 588 | { 589 | var test = GetCode(@" 590 | class TestI { 591 | } 592 | [Immutable] 593 | class Test 594 | { 595 | private readonly TestI x; 596 | private readonly KeyValuePair y; 597 | } 598 | "); 599 | var expected1 = new DiagnosticResult 600 | { 601 | Id = "IMM003", 602 | Message = "Type of field 'x' is not immutable", 603 | Severity = DiagnosticSeverity.Error, 604 | Locations = 605 | new[] { 606 | new DiagnosticResultLocation("Test0.cs", 18, 36) 607 | } 608 | }; 609 | 610 | var expected2 = new DiagnosticResult 611 | { 612 | Id = "IMM003", 613 | Message = "Type of field 'y' is not immutable because type argument 'TestI' is not immutable", 614 | Severity = DiagnosticSeverity.Error, 615 | Locations = 616 | new[] { 617 | new DiagnosticResultLocation("Test0.cs", 19, 55) 618 | } 619 | }; 620 | 621 | VerifyCSharpDiagnostic(test, expected1, expected2); 622 | } 623 | 624 | [Fact] 625 | public void IMM004MemberPropsWhitelistedByConvention() 626 | { 627 | var test = GetCode(@" 628 | public enum TestEnum { 629 | A 630 | } 631 | [Immutable] 632 | public sealed class Test 633 | { 634 | public TestEnum x {get;} 635 | public byte a {get;} 636 | public char b {get;} 637 | public sbyte c {get;} 638 | public short d {get;} 639 | public ushort e {get;} 640 | public int f {get;} 641 | public uint g {get;} 642 | public long h {get;} 643 | public ulong i {get;} 644 | public string j {get;} 645 | public DateTime k {get;} 646 | public float l {get;} 647 | public double m {get;} 648 | public decimal n {get;} 649 | public object o {get;} 650 | public Guid p {get;} 651 | public TimeSpan q {get;} 652 | public DateTimeOffset r {get;} 653 | public int? s {get;} 654 | } 655 | "); 656 | VerifyCSharpDiagnostic(test); 657 | } 658 | 659 | [Fact] 660 | public void IMM004MemberPropsWhitelistedByConfiguration() 661 | { 662 | var code = GetCode(@" 663 | [Immutable] 664 | class Test 665 | { 666 | public Action a {get;} 667 | } 668 | "); 669 | 670 | string whitelist = $"System.Action"; 671 | var test = new CSharpCodeFixVerifier.Test(); 672 | test.TestCode = code; 673 | test.AdditionalFiles.Add(new AdditionalFile("ImmutableTypes.txt", whitelist)); 674 | test.Run(); 675 | } 676 | 677 | [Fact] 678 | public void IMM004MemberPropsImmutable() 679 | { 680 | var test = GetCode(@" 681 | [Immutable] 682 | class TestI { 683 | } 684 | [Immutable] 685 | class Test 686 | { 687 | private TestI x {get;} 688 | } 689 | "); 690 | VerifyCSharpDiagnostic(test); 691 | } 692 | 693 | [Fact] 694 | public void IMM004MemberPropsNotImmutable() 695 | { 696 | var test = GetCode(@" 697 | class TestI { 698 | } 699 | [Immutable] 700 | class Test 701 | { 702 | private TestI x {get; } 703 | } 704 | "); 705 | var expected = new DiagnosticResult 706 | { 707 | Id = "IMM004", 708 | Message = "Type of auto property 'x' is not immutable", 709 | Severity = DiagnosticSeverity.Error, 710 | Locations = 711 | new[] { 712 | new DiagnosticResultLocation("Test0.cs", 18, 27) 713 | } 714 | }; 715 | 716 | VerifyCSharpDiagnostic(test, expected); 717 | } 718 | 719 | [Fact] 720 | public void IMM004MemberPropsGenericNotImmutable() 721 | { 722 | var test = GetCode(@" 723 | class TestI { 724 | } 725 | [Immutable] 726 | class Test 727 | { 728 | private ImmutableDictionary x {get; } 729 | } 730 | "); 731 | var expected = new DiagnosticResult 732 | { 733 | Id = "IMM004", 734 | Message = "Type of auto property 'x' is not immutable because type argument 'TestI' is not immutable", 735 | Severity = DiagnosticSeverity.Error, 736 | Locations = 737 | new[] { 738 | new DiagnosticResultLocation("Test0.cs", 18, 54) 739 | } 740 | }; 741 | 742 | VerifyCSharpDiagnostic(test, expected); 743 | } 744 | 745 | [Fact] 746 | public void IMM004MemberPropsGenericImmutable() 747 | { 748 | var test = GetCode(@" 749 | [Immutable] 750 | class TestI { 751 | } 752 | [Immutable] 753 | class Test 754 | { 755 | private ImmutableArray? x {get; } 756 | } 757 | "); 758 | VerifyCSharpDiagnostic(test); 759 | } 760 | 761 | [Fact] 762 | public void IMM005NormalConstructor() 763 | { 764 | var test = GetCode(@" 765 | [Immutable] 766 | class Test 767 | { 768 | private readonly int x; 769 | public Test() 770 | { 771 | x = 5; 772 | this.x = 6; 773 | } 774 | } 775 | "); 776 | VerifyCSharpDiagnostic(test); 777 | } 778 | 779 | [Fact] 780 | public void IMM005NormalConstructor2() 781 | { 782 | var test = GetCode(@" 783 | [Immutable] 784 | class Test 785 | { 786 | public static void Method(Test t) 787 | {} 788 | public static Test Instance = new Test(); 789 | private readonly int x; 790 | public Test() 791 | { 792 | Method(Instance); 793 | } 794 | } 795 | "); 796 | VerifyCSharpDiagnostic(test); 797 | } 798 | 799 | [Fact] 800 | public void IMM005MethodCallExplicitThisParamInConstructor() 801 | { 802 | var test = GetCode(@" 803 | [Immutable] 804 | class Test 805 | { 806 | public static void Method(Test t) 807 | {} 808 | 809 | private readonly int x; 810 | Test() 811 | { 812 | Method(this); 813 | x = 5; 814 | } 815 | } 816 | "); 817 | var expected = new DiagnosticResult 818 | { 819 | Id = "IMM005", 820 | Message = "Possibly incorrect usage of 'this' in the constructor of an immutable type", 821 | Severity = DiagnosticSeverity.Warning, 822 | Locations = 823 | new[] { 824 | new DiagnosticResultLocation("Test0.cs", 22, 24) 825 | } 826 | }; 827 | 828 | VerifyCSharpDiagnostic(test, expected); 829 | } 830 | 831 | [Fact] 832 | public void IMM005MethodCallIndirectThisParamInConstructor() 833 | { 834 | var test = GetCode(@" 835 | [Immutable] 836 | class Test 837 | { 838 | public static void Method(Test t) 839 | {} 840 | 841 | private readonly int x; 842 | Test() 843 | { 844 | var asd = this; 845 | Method(asd); 846 | x = 5; 847 | } 848 | } 849 | "); 850 | var expected = new DiagnosticResult 851 | { 852 | Id = "IMM005", 853 | Message = "Possibly incorrect usage of 'this' in the constructor of an immutable type", 854 | Severity = DiagnosticSeverity.Warning, 855 | Locations = 856 | new[] { 857 | new DiagnosticResultLocation("Test0.cs", 22, 27) 858 | } 859 | }; 860 | 861 | VerifyCSharpDiagnostic(test, expected); 862 | } 863 | 864 | [Fact] 865 | public void IMM005AssignThisToStaticInConstructor() 866 | { 867 | var test = GetCode(@" 868 | [Immutable] 869 | class Test 870 | { 871 | public static Test asd; 872 | private readonly int x; 873 | Test() 874 | { 875 | asd = this; 876 | x = 5; 877 | } 878 | } 879 | "); 880 | var expected = new DiagnosticResult 881 | { 882 | Id = "IMM005", 883 | Message = "Possibly incorrect usage of 'this' in the constructor of an immutable type", 884 | Severity = DiagnosticSeverity.Warning, 885 | Locations = 886 | new[] { 887 | new DiagnosticResultLocation("Test0.cs", 20, 23) 888 | } 889 | }; 890 | 891 | VerifyCSharpDiagnostic(test, expected); 892 | } 893 | 894 | [Fact] 895 | public void IMM005CaptureThisInConstructor() 896 | { 897 | var test = GetCode(@" 898 | [Immutable] 899 | class Test 900 | { 901 | public static void Method(Func t) 902 | { 903 | t(); 904 | } 905 | 906 | private readonly int x; 907 | Test() 908 | { 909 | Method(() => this.x); 910 | x = 5; 911 | } 912 | } 913 | "); 914 | var expected = new DiagnosticResult 915 | { 916 | Id = "IMM005", 917 | Message = "Possibly incorrect usage of 'this' in the constructor of an immutable type", 918 | Severity = DiagnosticSeverity.Warning, 919 | Locations = 920 | new[] { 921 | new DiagnosticResultLocation("Test0.cs", 24, 24) 922 | } 923 | }; 924 | 925 | VerifyCSharpDiagnostic(test, expected); 926 | } 927 | 928 | [Fact] 929 | public void IMM005CaptureThisInConstructorAllowed() 930 | { 931 | var test = GetCode(@" 932 | class Test 933 | { 934 | public static void Method(Func t) 935 | { 936 | t(); 937 | } 938 | 939 | private readonly int x; 940 | Test() 941 | { 942 | Method(() => this.x); 943 | x = 5; 944 | } 945 | } 946 | "); 947 | VerifyCSharpDiagnostic(test); 948 | } 949 | 950 | [Fact] 951 | public void IMM006BaseTypeStruct() 952 | { 953 | var test = GetCode(@" 954 | [Immutable] 955 | struct Test 956 | { 957 | } 958 | "); 959 | VerifyCSharpDiagnostic(test); 960 | } 961 | 962 | [Fact] 963 | public void IMM006BaseTypeObject() 964 | { 965 | var test = GetCode(@" 966 | [Immutable] 967 | class Test : object 968 | { 969 | } 970 | "); 971 | VerifyCSharpDiagnostic(test); 972 | } 973 | 974 | [Fact] 975 | public void IMM006BaseTypeImmutable() 976 | { 977 | var test = GetCode(@" 978 | [Immutable] 979 | class Test1 980 | { 981 | } 982 | 983 | [Immutable] 984 | class Test2 : Test1 985 | { 986 | } 987 | "); 988 | VerifyCSharpDiagnostic(test); 989 | } 990 | 991 | [Fact] 992 | public void IMM006BaseTypeNotImmutable() 993 | { 994 | var test = GetCode(@" 995 | class Test1 996 | { 997 | } 998 | 999 | [Immutable] 1000 | class Test2 : Test1 1001 | { 1002 | } 1003 | "); 1004 | var expected = new DiagnosticResult 1005 | { 1006 | Id = "IMM006", 1007 | Message = "Type 'Test2' base type must be 'object' or immutable", 1008 | Severity = DiagnosticSeverity.Error, 1009 | Locations = 1010 | new[] { 1011 | new DiagnosticResultLocation("Test0.cs", 18, 15) 1012 | } 1013 | }; 1014 | 1015 | VerifyCSharpDiagnostic(test, expected); 1016 | } 1017 | 1018 | [Fact] 1019 | public void IMM006BaseTypeNotImmutableGeneric() 1020 | { 1021 | var test = GetCode(@" 1022 | class MutableClass { 1023 | public int X; 1024 | } 1025 | [Immutable] 1026 | class Test1 1027 | { 1028 | public readonly T Value; 1029 | } 1030 | 1031 | [Immutable] 1032 | class Test2 : Test1 1033 | { 1034 | } 1035 | "); 1036 | var expected = new DiagnosticResult 1037 | { 1038 | Id = "IMM006", 1039 | Message = "Type 'Test2' base type must be 'object' or immutable", 1040 | Severity = DiagnosticSeverity.Error, 1041 | Locations = 1042 | new[] { 1043 | new DiagnosticResultLocation("Test0.cs", 23, 15) 1044 | } 1045 | }; 1046 | 1047 | VerifyCSharpDiagnostic(test, expected); 1048 | } 1049 | 1050 | [Fact] 1051 | public void IMM007DerivedTypeNotImmutable() 1052 | { 1053 | var test = GetCode(@" 1054 | [Immutable] 1055 | class Test1 1056 | { 1057 | } 1058 | 1059 | class Test2 : Test1 1060 | { 1061 | } 1062 | "); 1063 | var expected = new DiagnosticResult 1064 | { 1065 | Id = "IMM007", 1066 | Message = "Type 'Test2' must be immutable because it derives from 'Test1'", 1067 | Severity = DiagnosticSeverity.Error, 1068 | Locations = 1069 | new[] { 1070 | new DiagnosticResultLocation("Test0.cs", 18, 15) 1071 | } 1072 | }; 1073 | 1074 | VerifyCSharpDiagnostic(test, expected); 1075 | } 1076 | 1077 | [Fact] 1078 | public void IMM007DerivedFromInterfaceTypeNotImmutable() 1079 | { 1080 | var test = GetCode(@" 1081 | [Immutable] 1082 | interface Test1 1083 | { 1084 | } 1085 | 1086 | class Test2 : Test1 1087 | { 1088 | } 1089 | "); 1090 | var expected = new DiagnosticResult 1091 | { 1092 | Id = "IMM007", 1093 | Message = "Type 'Test2' must be immutable because it derives from 'Test1'", 1094 | Severity = DiagnosticSeverity.Error, 1095 | Locations = 1096 | new[] { 1097 | new DiagnosticResultLocation("Test0.cs", 18, 15) 1098 | } 1099 | }; 1100 | 1101 | VerifyCSharpDiagnostic(test, expected); 1102 | } 1103 | 1104 | [Fact] 1105 | public void IMM008CapturePartiallyConstructedInstanceReferenceInInitOnlyProperty() 1106 | { 1107 | var test = GetCode(@" 1108 | [Immutable] 1109 | class Test 1110 | { 1111 | private readonly int x; 1112 | public int X { 1113 | get { return x; } 1114 | init { 1115 | x = Test2.M(this); 1116 | } 1117 | } 1118 | } 1119 | 1120 | class Test2 { 1121 | public static int M(Test t) { 1122 | return t.X + 1; 1123 | } 1124 | 1125 | public void T() { 1126 | var t = new Test { 1127 | X = 1 1128 | }; 1129 | } 1130 | } 1131 | "); 1132 | var expected = new DiagnosticResult 1133 | { 1134 | Id = "IMM008", 1135 | Message = "Possibly incorrect usage of 'this' in an init only property method of an immutable type", 1136 | Severity = DiagnosticSeverity.Warning, 1137 | Locations = 1138 | new[] { 1139 | new DiagnosticResultLocation("Test0.cs", 20, 33) 1140 | } 1141 | }; 1142 | 1143 | VerifyCSharpDiagnostic(test, expected); 1144 | } 1145 | 1146 | private void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected) 1147 | { 1148 | CSharpCodeFixVerifier.VerifyAnalyzer(source, expected); 1149 | } 1150 | 1151 | private void VerifyCSharpFix(string oldSource, DiagnosticResult[] expected, string newSource) 1152 | { 1153 | CSharpCodeFixVerifier.VerifyCodeFix(oldSource, expected, newSource); 1154 | } 1155 | 1156 | private string GetCode(string code, string namesp = "ConsoleApplication1") 1157 | { 1158 | return @" 1159 | using System; 1160 | using System.Collections.Generic; 1161 | using System.Linq; 1162 | using System.Text; 1163 | using System.Threading.Tasks; 1164 | using System.Diagnostics; 1165 | using System.Collections.Immutable; 1166 | 1167 | namespace " + namesp + @" 1168 | { 1169 | " + code + @" 1170 | 1171 | class Program 1172 | { 1173 | public static void Main() {} 1174 | } 1175 | }"; 1176 | } 1177 | 1178 | } 1179 | } 1180 | --------------------------------------------------------------------------------