├── 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 | [](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 |
--------------------------------------------------------------------------------