├── .editorconfig ├── .gitattributes ├── .gitignore ├── DateTimeProvider.sln ├── LICENSE ├── README.md └── src ├── DateTimeProvider.Tests ├── DateTimeProvider.Tests.csproj ├── MultiThreadingTest1.cs ├── MultiThreadingTest2.cs └── Properties │ └── AssemblyInfo.cs ├── DateTimeProvider ├── DateTimeProvider.cs ├── DateTimeProvider.csproj ├── DateTimeProvider.nuspec ├── IDateTimeProvider.cs ├── LocalDateTimeProvider.cs ├── OverrideDateTimeProvider.cs ├── StaticDateTimeProvider.cs └── UtcDateTimeProvider.cs ├── DateTimeProviderAnalyser.Tests ├── DateTimeOffsetTests.cs ├── DateTimeProviderAnalyser.Tests.csproj ├── LocalDateTimeTests.cs ├── TestHelpers │ ├── CodeFixVerifier.cs │ ├── DiagnosticResult.cs │ ├── DiagnosticResultLocation.cs │ └── DiagnosticVerifier.cs └── UtcDateTimeTests.cs └── DateTimeProviderAnalyser ├── DateTimeNow ├── DateTimeNowAnalyser.cs └── DateTimeNowCodeFix.cs ├── DateTimeOffsetNow ├── DateTimeOffsetNowAnalyser.cs └── DateTimeOffsetNowCodeFix.cs ├── DateTimeProviderAnalyser.csproj ├── DateTimeProviderAnalyser.nuspec ├── DateTimeUtcNow ├── DateTimeUtcNowAnalyser.cs └── DateTimeUtcNowCodeFix.cs └── tools ├── install.ps1 └── uninstall.ps1 /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig (http://editorconfig.org) is awesome. It defines and maintains consistent coding styles between different editors and IDEs. 2 | # IDE Extensions 3 | # Visual Studio, install https://visualstudiogallery.msdn.microsoft.com/c8bccfe2-650c-4b42-bc5c-845e21f96328 4 | # VS Code, install https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig 5 | 6 | # This is the root .editorconfig 7 | root = true 8 | 9 | [*] 10 | end_of_line = crlf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | indent_style = space 14 | 15 | [*.cs] 16 | indent_style = space 17 | indent_size = 4 18 | 19 | [*.{json,config,xml}] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set 2 | * text=auto eol=crlf 3 | 4 | # Markdown is text-based markup language 5 | *.md text diff 6 | 7 | # These files are truly binary and should not be modified by git 8 | *.png binary 9 | *.jpg binary 10 | *.gif binary 11 | *.exe binary 12 | *.dll binary 13 | *.lnk binary 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | 28 | # Others 29 | *_i.c 30 | *_p.c 31 | *_i.h 32 | *.ilk 33 | *.meta 34 | *.obj 35 | *.pch 36 | *.pdb 37 | *.pgc 38 | *.pgd 39 | *.rsp 40 | *.sbr 41 | *.tlb 42 | *.tli 43 | *.tlh 44 | *.tmp 45 | *.tmp_proj 46 | *.log 47 | *.vspscc 48 | *.vssscc 49 | .builds 50 | *.pidb 51 | *.svclog 52 | *.scc 53 | 54 | # Visual Studio profiler 55 | *.psess 56 | *.vsp 57 | *.vspx 58 | 59 | # ReSharper is a .NET coding add-in 60 | _ReSharper*/ 61 | *.[Rr]e[Ss]harper 62 | *.DotSettings.user 63 | 64 | # NuGet Packages 65 | *.nupkg 66 | # The packages folder can be ignored because of Package Restore 67 | **/packages/* 68 | # except build/, which is used as an MSBuild target. 69 | !**/packages/build/ 70 | # Uncomment if necessary however generally it will be regenerated when needed 71 | #!**/packages/repositories.config 72 | 73 | # Visual Studio cache files 74 | # files ending in .cache can be ignored 75 | *.[Cc]ache 76 | # but keep track of directories ending in .cache 77 | !*.[Cc]ache/ 78 | 79 | # Others 80 | ~$* 81 | *~ 82 | 83 | # Backup & report files from converting an old project file 84 | # to a newer Visual Studio version. Backup files are not needed, 85 | # because we have git ;-) 86 | _UpgradeReport_Files/ 87 | Backup*/ 88 | UpgradeLog*.XML 89 | UpgradeLog*.htm -------------------------------------------------------------------------------- /DateTimeProvider.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26403.7 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AA95DE03-AC90-41BF-B28F-96C0D27BA3C5}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | .gitattributes = .gitattributes 10 | .gitignore = .gitignore 11 | LICENSE = LICENSE 12 | README.md = README.md 13 | EndProjectSection 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DateTimeProvider", "src\DateTimeProvider\DateTimeProvider.csproj", "{05DCFEC4-55E7-49F4-BE9D-B7E57DA32D2A}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DateTimeProvider.Tests", "src\DateTimeProvider.Tests\DateTimeProvider.Tests.csproj", "{1C220A43-9FB5-44A6-AA36-FA6344690BC4}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Roslyn Analyser", "Roslyn Analyser", "{BB359DF8-7E3C-429D-A1BE-256E95170C8D}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DateTimeProviderAnalyser", "src\DateTimeProviderAnalyser\DateTimeProviderAnalyser.csproj", "{42B2C20B-8175-4A3E-8D1F-52CA44721910}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DateTimeProviderAnalyser.Tests", "src\DateTimeProviderAnalyser.Tests\DateTimeProviderAnalyser.Tests.csproj", "{A1D671F0-190E-42C0-8ACD-84E427FCF4B6}" 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Release|Any CPU = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {05DCFEC4-55E7-49F4-BE9D-B7E57DA32D2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {05DCFEC4-55E7-49F4-BE9D-B7E57DA32D2A}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {05DCFEC4-55E7-49F4-BE9D-B7E57DA32D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {05DCFEC4-55E7-49F4-BE9D-B7E57DA32D2A}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {42B2C20B-8175-4A3E-8D1F-52CA44721910}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {42B2C20B-8175-4A3E-8D1F-52CA44721910}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {42B2C20B-8175-4A3E-8D1F-52CA44721910}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {42B2C20B-8175-4A3E-8D1F-52CA44721910}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {A1D671F0-190E-42C0-8ACD-84E427FCF4B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {A1D671F0-190E-42C0-8ACD-84E427FCF4B6}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {A1D671F0-190E-42C0-8ACD-84E427FCF4B6}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {A1D671F0-190E-42C0-8ACD-84E427FCF4B6}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {1C220A43-9FB5-44A6-AA36-FA6344690BC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {1C220A43-9FB5-44A6-AA36-FA6344690BC4}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {1C220A43-9FB5-44A6-AA36-FA6344690BC4}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {1C220A43-9FB5-44A6-AA36-FA6344690BC4}.Release|Any CPU.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(SolutionProperties) = preSolution 49 | HideSolutionNode = FALSE 50 | EndGlobalSection 51 | GlobalSection(NestedProjects) = preSolution 52 | {42B2C20B-8175-4A3E-8D1F-52CA44721910} = {BB359DF8-7E3C-429D-A1BE-256E95170C8D} 53 | {A1D671F0-190E-42C0-8ACD-84E427FCF4B6} = {BB359DF8-7E3C-429D-A1BE-256E95170C8D} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dennis Roche 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DateTimeProvider [![Build Master](https://ci.appveyor.com/api/projects/status/9rmer97iudefls62/branch/master?svg=true)](https://ci.appveyor.com/project/dennisroche/datetimeprovider) [![NuGet Version](http://img.shields.io/nuget/v/DateTimeProvider.svg?style=flat)](https://www.nuget.org/packages/DateTimeProvider/) [![.NET Standard 1.3](https://img.shields.io/badge/.NET%20Core-netstandard1.3-lightgrey.svg)](https://github.com/dotnet/standard/blob/master/docs/versions/netstandard1.3.md) ![.NET Framework 4.5](https://img.shields.io/badge/.NET-4.5.2-lightgrey.svg) [![Join the chat at https://gitter.im/DateTimeProvider/Lobby](https://badges.gitter.im/DateTimeProvider/Lobby.svg)](https://gitter.im/DateTimeProvider/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | ================ 3 | 4 | Provides an interface IDateTimeProvider and static container. 5 | 6 | Targets `netstandard1.3` and `net45`. 7 | 8 | `DateTimeProvider` exists in the `global::` namespace to make usage easier. 9 | 10 | 11 | #### DateTimeOffset vs DateTime 12 | 13 | The library provides `DateTimeOffset`, not `DateTime`. Why? Read this [excellent answer on StackOverflow](http://stackoverflow.com/a/14268167/73025) 14 | 15 | `DateTimeOffset` is great, however there are use-cases where you need to use `DateTime` as Local or UTC. 16 | 17 | You can use `DateTimeOffset.LocalDateTime` and `DateTimeOffset.UtcDateTime` properties. For convenience, our static `DateTimeProvider` has these properties as `LocalNow` and `UtcNow`. 18 | 19 | 20 | How to use 21 | ============= 22 | 23 | Install the [Nuget](https://www.nuget.org/packages/DateTimeProvider) package. 24 | 25 | Install-Package DateTimeProvider 26 | 27 | Set the provider 28 | 29 | ``` 30 | DateTimeProvider.Provider = new UtcDateTimeProvider(); 31 | ``` 32 | 33 | Or 34 | 35 | ``` 36 | DateTimeProvider.Provider = new LocalDateTimeProvider(); 37 | ``` 38 | 39 | **NB** The default is `UtcDateTimeProvider`. 40 | 41 | Then use or replace in place of `DateTime` in your code. 42 | 43 | ```c# 44 | var now = DateTimeProvider.Now; 45 | var local = DateTimeProvider.LocalNow; 46 | var utc = DateTimeProvider.UtcNow; 47 | ``` 48 | 49 | Pinning DateTime 50 | ============= 51 | 52 | Also provided is a static `IDateTimeProvider` and `IDisposable` Override so that time can be manipulated in a fixed scope. 53 | 54 | Why is this useful? 55 | 56 | - In a web request, you want to freeze time so that all your modified dates are aligned (i.e. not +/- by a few seconds) 57 | - In unit tests when you need to verify business logic. 58 | 59 | ### Examples 60 | 61 | #### Pinning Time in Logic 62 | 63 | ```c# 64 | using (var o = new OverrideDateTimeProvider(DateTimeProvider.Now)) 65 | { 66 | // logic 67 | } 68 | ``` 69 | 70 | #### Pinning Time in Unit Tests 71 | 72 | In your tests, you need may need to manipulate time to verify your logic. This is easy using the `OverrideDateTimeProvider`. 73 | 74 | ```c# 75 | Console.WriteLine($"{DateTimeProvider.Now}"); 76 | 77 | var testingWithDate = new DateTimeOffset(new DateTime(2014, 10, 01), TimeSpan.FromHours(8)); 78 | using (var o = new OverrideDateTimeProvider(testingWithDate)) 79 | { 80 | Console.WriteLine($"{DateTimeProvider.Now} (Testing)"); 81 | o.MoveTimeForward(TimeSpan.FromHours(5)); 82 | Console.WriteLine($"{DateTimeProvider.Now} (+ 5 hours)"); 83 | } 84 | 85 | Console.WriteLine($"{DateTimeProvider.Now} (Restored)"); 86 | ``` 87 | 88 | Output 89 | 90 | ``` 91 | 6/11/2015 5:08:12 AM +00:00 92 | 1/10/2014 12:00:00 AM +08:00 (Testing) 93 | 1/10/2014 5:00:00 AM +08:00 (+ 5 hours) 94 | 6/11/2015 5:08:12 AM +00:00 (Restored) 95 | ``` 96 | 97 | 98 | DateTimeProvider.Analyser [![NuGet Version](http://img.shields.io/nuget/v/DateTimeProvider.Analyser.svg?style=flat)](https://www.nuget.org/packages/DateTimeProvider.Analyser/) 99 | ================ 100 | 101 | Also available is a Roslyn Analyser that will suggest replacements for `DateTime` for `DateTimeProvider`. 102 | 103 | Install the [Nuget](https://www.nuget.org/packages/DateTimeProvider.Analyser) package. 104 | 105 | Install-Package DateTimeProvider.Analyser 106 | 107 | 108 | License 109 | ============= 110 | 111 | MIT 112 | -------------------------------------------------------------------------------- /src/DateTimeProvider.Tests/DateTimeProvider.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp1.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/DateTimeProvider.Tests/MultiThreadingTest1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | // ReSharper disable once CheckNamespace 7 | namespace DateTimeProviders.Tests 8 | { 9 | public class MultiThreadingTest1 10 | { 11 | public MultiThreadingTest1() 12 | { 13 | DateTimeProvider.Provider = new UtcDateTimeProvider(); 14 | } 15 | 16 | [Fact] 17 | public void MoveTimeForward5HrsFromOffset0() 18 | { 19 | var culture = new CultureInfo("en-AU"); 20 | 21 | var testingWithDate = new DateTimeOffset(new DateTime(2014, 10, 01), TimeSpan.FromHours(0)); 22 | using (var o = new OverrideDateTimeProvider(testingWithDate)) 23 | { 24 | DateTimeProvider.Now.ToString(culture).ShouldBe("1/10/2014 12:00:00 AM +00:00"); 25 | o.MoveTimeForward(TimeSpan.FromHours(5)); 26 | DateTimeProvider.Now.ToString(culture).ShouldBe("1/10/2014 5:00:00 AM +00:00"); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/DateTimeProvider.Tests/MultiThreadingTest2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Shouldly; 4 | using Xunit; 5 | 6 | // ReSharper disable once CheckNamespace 7 | namespace DateTimeProviders.Tests 8 | { 9 | public class MultiThreadingTest2 10 | { 11 | public MultiThreadingTest2() 12 | { 13 | DateTimeProvider.Provider = new UtcDateTimeProvider(); 14 | } 15 | 16 | [Fact] 17 | public void MoveTimeForward5HrsFromOffset8() 18 | { 19 | var culture = new CultureInfo("en-AU"); 20 | 21 | var testingWithDate = new DateTimeOffset(new DateTime(2014, 10, 01), TimeSpan.FromHours(8)); 22 | using (var o = new OverrideDateTimeProvider(testingWithDate)) 23 | { 24 | DateTimeProvider.Now.ToString(culture).ShouldBe("1/10/2014 12:00:00 AM +08:00"); 25 | o.MoveTimeForward(TimeSpan.FromHours(5)); 26 | DateTimeProvider.Now.ToString(culture).ShouldBe("1/10/2014 5:00:00 AM +08:00"); 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/DateTimeProvider.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | // Ensure we're using XUnit default Test Parallelization 4 | [assembly: CollectionBehavior(DisableTestParallelization = false)] 5 | -------------------------------------------------------------------------------- /src/DateTimeProvider/DateTimeProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using DateTimeProviders; 4 | 5 | // ReSharper disable CheckNamespace 6 | // Using global:: namespace 7 | 8 | public static class DateTimeProvider 9 | { 10 | private static readonly ThreadLocal ProviderThreadLocal; 11 | 12 | static DateTimeProvider() 13 | { 14 | ProviderThreadLocal = new ThreadLocal(() => new UtcDateTimeProvider()); 15 | } 16 | 17 | public static DateTimeOffset Now => ProviderThreadLocal.Value.Now; 18 | public static DateTime LocalNow => ProviderThreadLocal.Value.Now.LocalDateTime; 19 | public static DateTime UtcNow => ProviderThreadLocal.Value.Now.UtcDateTime; 20 | 21 | public static IDateTimeProvider Provider 22 | { 23 | get => ProviderThreadLocal.Value; 24 | set => ProviderThreadLocal.Value = value; 25 | } 26 | } -------------------------------------------------------------------------------- /src/DateTimeProvider/DateTimeProvider.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net45;netstandard1.3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/DateTimeProvider/DateTimeProvider.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | DateTimeProvider 5 | $version$ 6 | DateTimeProvider 7 | dennisroche 8 | https://github.com/dennisroche/DateTimeProvider/ 9 | https://raw.githubusercontent.com/dennisroche/DateTimeProvider/master/LICENSE 10 | false 11 | Provides an interface IDataTimeProvider and static container. 12 | DateTime, DateTimeProvider, Date, Time 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/DateTimeProvider/IDateTimeProvider.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable once CheckNamespace 2 | namespace DateTimeProviders 3 | { 4 | public interface IDateTimeProvider 5 | { 6 | System.DateTimeOffset Now { get; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/DateTimeProvider/LocalDateTimeProvider.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable once CheckNamespace 2 | namespace DateTimeProviders 3 | { 4 | public class LocalDateTimeProvider : IDateTimeProvider 5 | { 6 | public System.DateTimeOffset Now => System.DateTimeOffset.Now; 7 | } 8 | } -------------------------------------------------------------------------------- /src/DateTimeProvider/OverrideDateTimeProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable once CheckNamespace 4 | namespace DateTimeProviders 5 | { 6 | public class OverrideDateTimeProvider : IDisposable 7 | { 8 | private readonly IDateTimeProvider _originalProvider; 9 | private readonly StaticDateTimeProvider _staticProvider; 10 | 11 | private OverrideDateTimeProvider(StaticDateTimeProvider staticProvider) 12 | { 13 | _staticProvider = staticProvider; 14 | _originalProvider = DateTimeProvider.Provider; 15 | 16 | DateTimeProvider.Provider = _staticProvider; 17 | } 18 | 19 | public OverrideDateTimeProvider() 20 | : this(new StaticDateTimeProvider()) 21 | { 22 | } 23 | 24 | public OverrideDateTimeProvider(DateTimeOffset now) 25 | : this(new StaticDateTimeProvider(now)) 26 | { 27 | } 28 | 29 | public OverrideDateTimeProvider(string now) 30 | : this(new StaticDateTimeProvider(now)) 31 | { 32 | } 33 | 34 | public void Dispose() 35 | { 36 | DateTimeProvider.Provider = _originalProvider; 37 | } 38 | 39 | public OverrideDateTimeProvider SetNow(string now) 40 | { 41 | _staticProvider.SetNow(now); 42 | return this; 43 | } 44 | 45 | public OverrideDateTimeProvider MoveTimeForward(TimeSpan amount) 46 | { 47 | _staticProvider.MoveTimeForward(amount); 48 | return this; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/DateTimeProvider/StaticDateTimeProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable once CheckNamespace 4 | namespace DateTimeProviders 5 | { 6 | public class StaticDateTimeProvider : IDateTimeProvider 7 | { 8 | private DateTimeOffset _now; 9 | 10 | public StaticDateTimeProvider() 11 | { 12 | _now = DateTimeOffset.UtcNow; 13 | } 14 | 15 | public StaticDateTimeProvider(DateTimeOffset now) 16 | { 17 | _now = now; 18 | } 19 | 20 | public StaticDateTimeProvider(string now) 21 | { 22 | _now = DateTimeOffset.Parse(now); 23 | } 24 | 25 | public DateTimeOffset Now 26 | { 27 | get { return _now; } 28 | } 29 | 30 | public StaticDateTimeProvider SetNow(string now) 31 | { 32 | _now = DateTimeOffset.Parse(now); 33 | return this; 34 | } 35 | 36 | public StaticDateTimeProvider MoveTimeForward(TimeSpan amount) 37 | { 38 | _now = _now.Add(amount); 39 | return this; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/DateTimeProvider/UtcDateTimeProvider.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable once CheckNamespace 2 | namespace DateTimeProviders 3 | { 4 | public class UtcDateTimeProvider : IDateTimeProvider 5 | { 6 | public System.DateTimeOffset Now => System.DateTimeOffset.UtcNow; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser.Tests/DateTimeOffsetTests.cs: -------------------------------------------------------------------------------- 1 | using DateTimeProviderAnalyser.DateTimeOffsetNow; 2 | using DateTimeProviderAnalyser.Tests.TestHelpers; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CodeFixes; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | using Xunit; 7 | 8 | namespace DateTimeProviderAnalyser.Tests 9 | { 10 | public class DateTimeOffsetTests : CodeFixVerifier 11 | { 12 | private const string SourceCodeWithIssue = @" 13 | using System; 14 | 15 | namespace ConsoleApplication1 16 | { 17 | class TypeName 18 | { 19 | public TypeName() 20 | { 21 | var now = DateTimeOffset.Now; 22 | } 23 | } 24 | }"; 25 | 26 | private const string SourceCodeWithFix = @" 27 | using System; 28 | 29 | namespace ConsoleApplication1 30 | { 31 | class TypeName 32 | { 33 | public TypeName() 34 | { 35 | var now = DateTimeProvider.Now; 36 | } 37 | } 38 | }"; 39 | 40 | protected override CodeFixProvider GetCSharpCodeFixProvider() 41 | { 42 | return new DateTimeOffsetNowCodeFix(); 43 | } 44 | 45 | protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() 46 | { 47 | return new DateTimeOffsetNowAnalyser(); 48 | } 49 | 50 | [Fact] 51 | public void ExpectNoDiagnosticResults() 52 | { 53 | const string source = @""; 54 | VerifyCSharpDiagnostic(source); 55 | } 56 | 57 | [Fact] 58 | public void IdentifySuggestedFix() 59 | { 60 | var expected = new DiagnosticResult 61 | { 62 | Id = "DateTimeOffsetNowAnalyser", 63 | Message = "Use DateTimeProvider.Now instead of DateTimeOffset.Now", 64 | Severity = DiagnosticSeverity.Warning, 65 | Locations = new[] {new DiagnosticResultLocation("Test0.cs", 10, 27)} 66 | }; 67 | 68 | VerifyCSharpDiagnostic(SourceCodeWithIssue, expected); 69 | } 70 | 71 | [Fact] 72 | public void ApplySuggestedFix() 73 | { 74 | var expected = new DiagnosticResult 75 | { 76 | Id = "DateTimeOffsetNowAnalyser", 77 | Message = "Use DateTimeProvider.Now instead of DateTimeOffset.Now", 78 | Severity = DiagnosticSeverity.Warning, 79 | Locations = new[] {new DiagnosticResultLocation("Test0.cs", 10, 27)} 80 | }; 81 | 82 | VerifyCSharpDiagnostic(SourceCodeWithIssue, expected); 83 | VerifyCSharpFix(SourceCodeWithIssue, SourceCodeWithFix, null, true); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser.Tests/DateTimeProviderAnalyser.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | netcoreapp1.1 6 | $(PackageTargetFallback);portable-net45+win8+wp8+wpa81 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser.Tests/LocalDateTimeTests.cs: -------------------------------------------------------------------------------- 1 | using DateTimeProviderAnalyser.DateTimeNow; 2 | using DateTimeProviderAnalyser.Tests.TestHelpers; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CodeFixes; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | using Xunit; 7 | 8 | namespace DateTimeProviderAnalyser.Tests 9 | { 10 | public class LocalDateTimeTests : CodeFixVerifier 11 | { 12 | private const string SourceCodeWithIssue = @" 13 | using System; 14 | 15 | namespace ConsoleApplication1 16 | { 17 | class TypeName 18 | { 19 | public TypeName() 20 | { 21 | var now = DateTime.Now; 22 | } 23 | } 24 | }"; 25 | 26 | private const string SourceCodeWithFix = @" 27 | using System; 28 | 29 | namespace ConsoleApplication1 30 | { 31 | class TypeName 32 | { 33 | public TypeName() 34 | { 35 | var now = DateTimeProvider.LocalNow; 36 | } 37 | } 38 | }"; 39 | 40 | protected override CodeFixProvider GetCSharpCodeFixProvider() 41 | { 42 | return new DateTimeNowCodeFix(); 43 | } 44 | 45 | protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() 46 | { 47 | return new DateTimeNowAnalyser(); 48 | } 49 | 50 | [Fact] 51 | public void ExpectNoDiagnosticResults() 52 | { 53 | const string source = @""; 54 | VerifyCSharpDiagnostic(source); 55 | } 56 | 57 | [Fact] 58 | public void IdentifySuggestedFix() 59 | { 60 | var expected = new DiagnosticResult 61 | { 62 | Id = "DateTimeNowAnalyser", 63 | Message = "Use DateTimeProvider.LocalNow instead of DateTime.Now", 64 | Severity = DiagnosticSeverity.Warning, 65 | Locations = new[] { new DiagnosticResultLocation("Test0.cs", 10, 27) } 66 | }; 67 | 68 | VerifyCSharpDiagnostic(SourceCodeWithIssue, expected); 69 | } 70 | 71 | [Fact] 72 | public void ApplySuggestedFix() 73 | { 74 | var expected = new DiagnosticResult 75 | { 76 | Id = "DateTimeNowAnalyser", 77 | Message = "Use DateTimeProvider.LocalNow instead of DateTime.Now", 78 | Severity = DiagnosticSeverity.Warning, 79 | Locations = new[] { new DiagnosticResultLocation("Test0.cs", 10, 27) } 80 | }; 81 | 82 | VerifyCSharpDiagnostic(SourceCodeWithIssue, expected); 83 | VerifyCSharpFix(SourceCodeWithIssue, SourceCodeWithFix, null, true); 84 | } 85 | 86 | } 87 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser.Tests/TestHelpers/CodeFixVerifier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CodeActions; 7 | using Microsoft.CodeAnalysis.CodeFixes; 8 | using Microsoft.CodeAnalysis.Diagnostics; 9 | using Microsoft.CodeAnalysis.Formatting; 10 | using Microsoft.CodeAnalysis.Simplification; 11 | using Xunit; 12 | 13 | namespace DateTimeProviderAnalyser.Tests.TestHelpers 14 | { 15 | /// 16 | /// Contains methods used to verify correctness of codefixes 17 | /// 18 | public abstract class CodeFixVerifier : DiagnosticVerifier 19 | { 20 | /// 21 | /// Returns the codefix being tested (C#) 22 | /// 23 | /// The CodeFixProvider to be used for CSharp code 24 | protected virtual CodeFixProvider GetCSharpCodeFixProvider() 25 | { 26 | throw new NotImplementedException(); 27 | } 28 | 29 | /// 30 | /// Called to test a C# codefix when applied on the inputted string as a source 31 | /// 32 | /// A class in the form of a string before the CodeFix was applied to it 33 | /// A class in the form of a string after the CodeFix was applied to it 34 | /// Index determining which codefix to apply if there are multiple 35 | /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied 36 | public void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) 37 | { 38 | VerifyFix(GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); 39 | } 40 | 41 | /// 42 | /// General verifier for codefixes. 43 | /// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes. 44 | /// Then gets the string after the codefix is applied and compares it with the expected result. 45 | /// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true. 46 | /// 47 | /// The analyzer to be applied to the source code 48 | /// The codefix to be applied to the code wherever the relevant Diagnostic is found 49 | /// A class in the form of a string before the CodeFix was applied to it 50 | /// A class in the form of a string after the CodeFix was applied to it 51 | /// Index determining which codefix to apply if there are multiple 52 | /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied 53 | private static void VerifyFix(DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics) 54 | { 55 | var document = CreateDocument(oldSource); 56 | var analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); 57 | var compilerDiagnostics = GetCompilerDiagnostics(document).ToArray(); 58 | 59 | var attempts = analyzerDiagnostics.Length; 60 | 61 | for (var attempt = 0; attempt < attempts; ++attempt) 62 | { 63 | var actions = new List(); 64 | var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None); 65 | codeFixProvider.RegisterCodeFixesAsync(context).Wait(); 66 | 67 | if (!actions.Any()) 68 | { 69 | break; 70 | } 71 | 72 | if (codeFixIndex != null) 73 | { 74 | document = ApplyFix(document, actions.ElementAt((int)codeFixIndex)); 75 | break; 76 | } 77 | 78 | document = ApplyFix(document, actions.ElementAt(0)); 79 | analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); 80 | 81 | var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); 82 | 83 | // Check if applying the code fix introduced any new compiler diagnostics 84 | if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any()) 85 | { 86 | // Format and get the compiler diagnostics again so that the locations make sense in the output 87 | document = document.WithSyntaxRoot(Formatter.Format(document.GetSyntaxRootAsync().Result, Formatter.Annotation, document.Project.Solution.Workspace)); 88 | newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); 89 | 90 | Assert.True(false, 91 | $"Fix introduced new compiler diagnostics:\r\n{string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString()))}\r\n\r\nNew document:\r\n{document.GetSyntaxRootAsync().Result.ToFullString()}\r\n"); 92 | } 93 | 94 | // Check if there are analyzer diagnostics left after the code fix 95 | if (!analyzerDiagnostics.Any()) 96 | { 97 | break; 98 | } 99 | } 100 | 101 | // After applying all of the code fixes, compare the resulting string to the inputted one 102 | var actual = GetStringFromDocument(document); 103 | Assert.Equal(newSource, actual); 104 | } 105 | 106 | /// 107 | /// Apply the inputted CodeAction to the inputted document. 108 | /// Meant to be used to apply codefixes. 109 | /// 110 | /// The Document to apply the fix on 111 | /// A CodeAction that will be applied to the Document. 112 | /// A Document with the changes from the CodeAction 113 | private static Document ApplyFix(Document document, CodeAction codeAction) 114 | { 115 | var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result; 116 | var solution = operations.OfType().Single().ChangedSolution; 117 | return solution.GetDocument(document.Id); 118 | } 119 | 120 | /// 121 | /// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection. 122 | /// Note: Considers Diagnostics to be the same if they have the same Ids. In the case of multiple diagnostics with the same Id in a row, 123 | /// this method may not necessarily return the new one. 124 | /// 125 | /// The Diagnostics that existed in the code before the CodeFix was applied 126 | /// The Diagnostics that exist in the code after the CodeFix was applied 127 | /// A list of Diagnostics that only surfaced in the code after the CodeFix was applied 128 | private static IEnumerable GetNewDiagnostics(IEnumerable diagnostics, IEnumerable newDiagnostics) 129 | { 130 | var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 131 | var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 132 | 133 | var oldIndex = 0; 134 | var newIndex = 0; 135 | 136 | while (newIndex < newArray.Length) 137 | { 138 | if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id) 139 | { 140 | ++oldIndex; 141 | ++newIndex; 142 | } 143 | else 144 | { 145 | yield return newArray[newIndex++]; 146 | } 147 | } 148 | } 149 | 150 | /// 151 | /// Get the existing compiler diagnostics on the inputted document. 152 | /// 153 | /// The Document to run the compiler diagnostic analyzers on 154 | /// The compiler diagnostics that were found in the code 155 | private static IEnumerable GetCompilerDiagnostics(Document document) 156 | { 157 | return document.GetSemanticModelAsync().Result.GetDiagnostics(); 158 | } 159 | 160 | /// 161 | /// Given a document, turn it into a string based on the syntax root 162 | /// 163 | /// The Document to be converted to a string 164 | /// A string containing the syntax of the Document after formatting 165 | private static string GetStringFromDocument(Document document) 166 | { 167 | var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result; 168 | var root = simplifiedDoc.GetSyntaxRootAsync().Result; 169 | root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace); 170 | 171 | return root.GetText().ToString(); 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser.Tests/TestHelpers/DiagnosticResult.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace DateTimeProviderAnalyser.Tests.TestHelpers 4 | { 5 | public struct DiagnosticResult 6 | { 7 | public string Path => Locations.Length > 0 ? Locations[0].Path : ""; 8 | public int Line => Locations.Length > 0 ? Locations[0].Line : -1; 9 | public int Column => Locations.Length > 0 ? Locations[0].Column : -1; 10 | 11 | public DiagnosticResultLocation[] Locations 12 | { 13 | get => _locations ?? (_locations = new DiagnosticResultLocation[] {}); 14 | set => _locations = value; 15 | } 16 | 17 | public DiagnosticSeverity Severity { get; set; } 18 | public string Id { get; set; } 19 | public string Message { get; set; } 20 | 21 | private DiagnosticResultLocation[] _locations; 22 | } 23 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser.Tests/TestHelpers/DiagnosticResultLocation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DateTimeProviderAnalyser.Tests.TestHelpers 4 | { 5 | /// 6 | /// Location where the diagnostic appears, as determined by path, line number, and column number. 7 | /// 8 | public struct DiagnosticResultLocation 9 | { 10 | public DiagnosticResultLocation(string path, int line, int column) 11 | { 12 | if (line < -1) 13 | { 14 | throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); 15 | } 16 | 17 | if (column < -1) 18 | { 19 | throw new ArgumentOutOfRangeException(nameof(line), "column must be >= -1"); 20 | } 21 | 22 | Path = path; 23 | Line = line; 24 | Column = column; 25 | } 26 | 27 | public string Path { get; } 28 | public int Line { get; } 29 | public int Column { get; } 30 | } 31 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser.Tests/TestHelpers/DiagnosticVerifier.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 Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CSharp; 9 | using Microsoft.CodeAnalysis.Diagnostics; 10 | using Microsoft.CodeAnalysis.Text; 11 | using Xunit; 12 | 13 | namespace DateTimeProviderAnalyser.Tests.TestHelpers 14 | { 15 | /// 16 | /// Superclass of all Unit Tests for DiagnosticAnalyzers 17 | /// 18 | public abstract class DiagnosticVerifier 19 | { 20 | private static readonly MetadataReference CorlibReference = CreateMetadataReferenceFromAssembly(typeof(object).GetTypeInfo().Assembly); 21 | private static readonly MetadataReference SystemCoreReference = CreateMetadataReferenceFromAssembly(typeof(Enumerable).GetTypeInfo().Assembly); 22 | private static readonly MetadataReference CSharpSymbolsReference = CreateMetadataReferenceFromAssembly(typeof(CSharpCompilation).GetTypeInfo().Assembly); 23 | private static readonly MetadataReference CodeAnalysisReference = CreateMetadataReferenceFromAssembly(typeof(Compilation).GetTypeInfo().Assembly); 24 | 25 | private const string DefaultFilePathPrefix = "Test"; 26 | private const string CSharpDefaultFileExt = "cs"; 27 | private const string TestProjectName = "TestProject"; 28 | 29 | private static MetadataReference CreateMetadataReferenceFromAssembly(Assembly assembly) 30 | { 31 | var method = typeof(MetadataReference) 32 | .GetTypeInfo() 33 | .GetMethod("CreateFromAssembly", new[] {typeof(Assembly)}); 34 | 35 | return (MetadataReference) method.Invoke(null, new object[] { assembly }); 36 | } 37 | 38 | /// 39 | /// Get the CSharp analyzer being tested 40 | /// 41 | protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() 42 | { 43 | throw new NotImplementedException(); 44 | } 45 | 46 | /// 47 | /// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source 48 | /// Note: input a DiagnosticResult for each Diagnostic expected 49 | /// 50 | /// A class in the form of a string to run the analyzer on 51 | /// DiagnosticResults that should appear after the analyzer is run on the source 52 | public void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected) 53 | { 54 | VerifyDiagnostics(new[] { source }, GetCSharpDiagnosticAnalyzer(), expected); 55 | } 56 | 57 | /// 58 | /// General method that gets a collection of actual diagnostics found in the source after the analyzer is run, 59 | /// then verifies each of them. 60 | /// 61 | /// An array of strings to create source documents from to run the analyzers on 62 | /// The analyzer to be run on the source code 63 | /// DiagnosticResults that should appear after the analyzer is run on the sources 64 | private static void VerifyDiagnostics(string[] sources, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected) 65 | { 66 | var diagnostics = GetSortedDiagnostics(sources, analyzer); 67 | VerifyDiagnosticResults(diagnostics, analyzer, expected); 68 | } 69 | 70 | /// 71 | /// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results. 72 | /// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic. 73 | /// 74 | /// The Diagnostics found by the compiler after running the analyzer on the source code 75 | /// The analyzer that was being run on the sources 76 | /// Diagnostic Results that should have appeared in the code 77 | private static void VerifyDiagnosticResults(IEnumerable actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults) 78 | { 79 | var expectedCount = expectedResults.Length; 80 | 81 | var actualDiagnostics = actualResults as Diagnostic[] ?? actualResults.ToArray(); 82 | var actualCount = actualDiagnostics.Length; 83 | 84 | if (expectedCount != actualCount) 85 | { 86 | var diagnosticsOutput = actualDiagnostics.Any() ? FormatDiagnostics(analyzer, actualDiagnostics.ToArray()) : " NONE."; 87 | 88 | Assert.True(false, 89 | $"Mismatch between number of diagnostics returned, expected \"{expectedCount}\" actual \"{actualCount}\"\r\n\r\nDiagnostics:\r\n{diagnosticsOutput}\r\n"); 90 | } 91 | 92 | for (var i = 0; i < expectedResults.Length; i++) 93 | { 94 | var actual = actualDiagnostics.ElementAt(i); 95 | var expected = expectedResults[i]; 96 | 97 | if (expected.Line == -1 && expected.Column == -1) 98 | { 99 | if (actual.Location != Location.None) 100 | { 101 | Assert.True(false, 102 | $"Expected:\nA project diagnostic with No location\nActual:\n{FormatDiagnostics(analyzer, actual)}"); 103 | } 104 | } 105 | else 106 | { 107 | VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First()); 108 | var additionalLocations = actual.AdditionalLocations.ToArray(); 109 | 110 | if (additionalLocations.Length != expected.Locations.Length - 1) 111 | { 112 | Assert.True(false, 113 | $"Expected {expected.Locations.Length - 1} additional locations but got {additionalLocations.Length} for Diagnostic:\r\n {FormatDiagnostics(analyzer, actual)}\r\n"); 114 | } 115 | 116 | for (var j = 0; j < additionalLocations.Length; ++j) 117 | { 118 | VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]); 119 | } 120 | } 121 | 122 | if (actual.Id != expected.Id) 123 | { 124 | Assert.True(false, 125 | $"Expected diagnostic id to be \"{expected.Id}\" was \"{actual.Id}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, actual)}\r\n"); 126 | } 127 | 128 | if (actual.Severity != expected.Severity) 129 | { 130 | Assert.True(false, 131 | $"Expected diagnostic severity to be \"{expected.Severity}\" was \"{actual.Severity}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, actual)}\r\n"); 132 | } 133 | 134 | if (actual.GetMessage() != expected.Message) 135 | { 136 | Assert.True(false, 137 | $"Expected diagnostic message to be \"{expected.Message}\" was \"{actual.GetMessage()}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, actual)}\r\n"); 138 | } 139 | } 140 | } 141 | 142 | /// 143 | /// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult. 144 | /// 145 | /// The analyzer that was being run on the sources 146 | /// The diagnostic that was found in the code 147 | /// The Location of the Diagnostic found in the code 148 | /// The DiagnosticResultLocation that should have been found 149 | private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected) 150 | { 151 | var actualSpan = actual.GetLineSpan(); 152 | 153 | Assert.True(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")), 154 | $"Expected diagnostic to be in file \"{expected.Path}\" was actually in file \"{actualSpan.Path}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, diagnostic)}\r\n"); 155 | 156 | var actualLinePosition = actualSpan.StartLinePosition; 157 | 158 | // Only check line position if there is an actual line in the real diagnostic 159 | if (actualLinePosition.Line > 0) 160 | { 161 | if (actualLinePosition.Line + 1 != expected.Line) 162 | { 163 | Assert.True(false, 164 | $"Expected diagnostic to be on line \"{expected.Line}\" was actually on line \"{actualLinePosition.Line + 1}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, diagnostic)}\r\n"); 165 | } 166 | } 167 | 168 | // Only check column position if there is an actual column position in the real diagnostic 169 | if (actualLinePosition.Character > 0) 170 | { 171 | if (actualLinePosition.Character + 1 != expected.Column) 172 | { 173 | Assert.True(false, 174 | $"Expected diagnostic to start at column \"{expected.Column}\" was actually at column \"{actualLinePosition.Character + 1}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, diagnostic)}\r\n"); 175 | } 176 | } 177 | } 178 | 179 | /// 180 | /// Helper method to format a Diagnostic into an easily readable string 181 | /// 182 | /// The analyzer that this verifier tests 183 | /// The Diagnostics to be formatted 184 | /// The Diagnostics formatted as a string 185 | private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics) 186 | { 187 | var builder = new StringBuilder(); 188 | for (var i = 0; i < diagnostics.Length; ++i) 189 | { 190 | builder.AppendLine("// " + diagnostics[i]); 191 | 192 | var analyzerType = analyzer.GetType(); 193 | var rules = analyzer.SupportedDiagnostics; 194 | 195 | foreach (var rule in rules) 196 | { 197 | if (rule != null && rule.Id == diagnostics[i].Id) 198 | { 199 | var location = diagnostics[i].Location; 200 | if (location == Location.None) 201 | { 202 | builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id); 203 | } 204 | else 205 | { 206 | Assert.True(location.IsInSource, 207 | $"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n"); 208 | 209 | var resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt"; 210 | var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition; 211 | 212 | builder.AppendFormat("{0}({1}, {2}, {3}.{4})", 213 | resultMethodName, 214 | linePosition.Line + 1, 215 | linePosition.Character + 1, 216 | analyzerType.Name, 217 | rule.Id); 218 | } 219 | 220 | if (i != diagnostics.Length - 1) 221 | { 222 | builder.Append(','); 223 | } 224 | 225 | builder.AppendLine(); 226 | break; 227 | } 228 | } 229 | } 230 | 231 | return builder.ToString(); 232 | } 233 | 234 | /// 235 | /// Given classes in the form of strings, their language, and an IDiagnosticAnlayzer to apply to it, return the diagnostics found in the string after converting it to a document. 236 | /// 237 | /// Classes in the form of strings 238 | /// The analyzer to be run on the sources 239 | /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location 240 | private static Diagnostic[] GetSortedDiagnostics(string[] sources, DiagnosticAnalyzer analyzer) 241 | { 242 | return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources)); 243 | } 244 | 245 | /// 246 | /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. 247 | /// The returned diagnostics are then ordered by location in the source document. 248 | /// 249 | /// The analyzer to run on the documents 250 | /// The Documents that the analyzer will be run on 251 | /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location 252 | protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents) 253 | { 254 | var projects = new HashSet(); 255 | foreach (var document in documents) 256 | { 257 | projects.Add(document.Project); 258 | } 259 | 260 | var diagnostics = new List(); 261 | foreach (var project in projects) 262 | { 263 | var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer)); 264 | var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result; 265 | foreach (var diag in diags) 266 | { 267 | if (diag.Location == Location.None || diag.Location.IsInMetadata) 268 | { 269 | diagnostics.Add(diag); 270 | } 271 | else 272 | { 273 | foreach (var document in documents) 274 | { 275 | var tree = document.GetSyntaxTreeAsync().Result; 276 | if (tree == diag.Location.SourceTree) 277 | { 278 | diagnostics.Add(diag); 279 | } 280 | } 281 | } 282 | } 283 | } 284 | 285 | var results = SortDiagnostics(diagnostics); 286 | diagnostics.Clear(); 287 | 288 | return results; 289 | } 290 | 291 | /// 292 | /// Sort diagnostics by location in source document 293 | /// 294 | /// The list of Diagnostics to be sorted 295 | /// An IEnumerable containing the Diagnostics in order of Location 296 | private static Diagnostic[] SortDiagnostics(IEnumerable diagnostics) 297 | { 298 | return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 299 | } 300 | 301 | /// 302 | /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it. 303 | /// 304 | /// Classes in the form of strings 305 | /// A Tuple containing the Documents produced from the sources and their TextSpans if relevant 306 | private static Document[] GetDocuments(string[] sources) 307 | { 308 | var project = CreateProject(sources); 309 | var documents = project.Documents.ToArray(); 310 | 311 | if (sources.Length != documents.Length) 312 | { 313 | throw new Exception("Amount of sources did not match amount of Documents created"); 314 | } 315 | 316 | return documents; 317 | } 318 | 319 | /// 320 | /// Create a Document from a string through creating a project that contains it. 321 | /// 322 | /// Classes in the form of a string 323 | /// A Document created from the source string 324 | protected static Document CreateDocument(string source) 325 | { 326 | return CreateProject(new[] { source }).Documents.First(); 327 | } 328 | 329 | /// 330 | /// Create a project using the inputted strings as sources. 331 | /// 332 | /// Classes in the form of strings 333 | /// A Project created out of the Documents created from the source strings 334 | private static Project CreateProject(string[] sources) 335 | { 336 | var fileNamePrefix = DefaultFilePathPrefix; 337 | var fileExt = CSharpDefaultFileExt; 338 | 339 | var projectId = ProjectId.CreateNewId(debugName: TestProjectName); 340 | 341 | var solution = new AdhocWorkspace() 342 | .CurrentSolution 343 | .AddProject(projectId, TestProjectName, TestProjectName, LanguageNames.CSharp) 344 | .AddMetadataReference(projectId, CorlibReference) 345 | .AddMetadataReference(projectId, SystemCoreReference) 346 | .AddMetadataReference(projectId, CSharpSymbolsReference) 347 | .AddMetadataReference(projectId, CodeAnalysisReference); 348 | 349 | var count = 0; 350 | foreach (var source in sources) 351 | { 352 | var newFileName = fileNamePrefix + count + "." + fileExt; 353 | var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); 354 | solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); 355 | count++; 356 | } 357 | 358 | return solution.GetProject(projectId); 359 | } 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser.Tests/UtcDateTimeTests.cs: -------------------------------------------------------------------------------- 1 | using DateTimeProviderAnalyser.DateTimeUtcNow; 2 | using DateTimeProviderAnalyser.Tests.TestHelpers; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CodeFixes; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | using Xunit; 7 | 8 | namespace DateTimeProviderAnalyser.Tests 9 | { 10 | public class UtcDateTimeTests : CodeFixVerifier 11 | { 12 | private const string SourceCodeWithIssue = @" 13 | using System; 14 | 15 | namespace ConsoleApplication1 16 | { 17 | class TypeName 18 | { 19 | public TypeName() 20 | { 21 | var now = DateTime.UtcNow; 22 | } 23 | } 24 | }"; 25 | 26 | private const string SourceCodeWithFix = @" 27 | using System; 28 | 29 | namespace ConsoleApplication1 30 | { 31 | class TypeName 32 | { 33 | public TypeName() 34 | { 35 | var now = DateTimeProvider.UtcNow; 36 | } 37 | } 38 | }"; 39 | 40 | protected override CodeFixProvider GetCSharpCodeFixProvider() 41 | { 42 | return new DateTimeUtcNowCodeFix(); 43 | } 44 | 45 | protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() 46 | { 47 | return new DateTimeUtcNowAnalyser(); 48 | } 49 | 50 | [Fact] 51 | public void ExpectNoDiagnosticResults() 52 | { 53 | const string source = @""; 54 | VerifyCSharpDiagnostic(source); 55 | } 56 | 57 | [Fact] 58 | public void IdentifySuggestedFix() 59 | { 60 | var expected = new DiagnosticResult 61 | { 62 | Id = "DateTimeUtcNowAnalyser", 63 | Message = "Use DateTimeProvider.UtcNow instead of DateTime.UtcNow", 64 | Severity = DiagnosticSeverity.Warning, 65 | Locations = new[] {new DiagnosticResultLocation("Test0.cs", 10, 27)} 66 | }; 67 | 68 | VerifyCSharpDiagnostic(SourceCodeWithIssue, expected); 69 | } 70 | 71 | [Fact] 72 | public void ApplySuggestedFix() 73 | { 74 | var expected = new DiagnosticResult 75 | { 76 | Id = "DateTimeUtcNowAnalyser", 77 | Message = "Use DateTimeProvider.UtcNow instead of DateTime.UtcNow", 78 | Severity = DiagnosticSeverity.Warning, 79 | Locations = new[] {new DiagnosticResultLocation("Test0.cs", 10, 27)} 80 | }; 81 | 82 | VerifyCSharpDiagnostic(SourceCodeWithIssue, expected); 83 | VerifyCSharpFix(SourceCodeWithIssue, SourceCodeWithFix, null, true); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser/DateTimeNow/DateTimeNowAnalyser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | 8 | namespace DateTimeProviderAnalyser.DateTimeNow 9 | { 10 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 11 | public class DateTimeNowAnalyser : DiagnosticAnalyzer 12 | { 13 | public const string DiagnosticId = nameof(DateTimeNowAnalyser); 14 | 15 | public const string Title = "Use DateTimeProvider.LocalNow instead of DateTime"; 16 | public const string MessageFormat = "Use DateTimeProvider.LocalNow instead of DateTime.Now"; 17 | public const string Description = "Use DateTimeProvider so that date and time is abstracted and easier to test"; 18 | public const string HelpLinkUri = "https://github.com/dennisroche/DateTimeProvider"; 19 | 20 | private const string Category = "Syntax"; 21 | private const bool AlwaysEnabledByDefault = true; 22 | 23 | public DateTimeNowAnalyser() 24 | { 25 | Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, AlwaysEnabledByDefault, Description, HelpLinkUri); 26 | SupportedDiagnostics = ImmutableArray.Create(Rule); 27 | } 28 | 29 | public DiagnosticDescriptor Rule { get; } 30 | public override ImmutableArray SupportedDiagnostics { get; } 31 | 32 | public override void Initialize(AnalysisContext context) 33 | { 34 | context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.SimpleMemberAccessExpression); 35 | } 36 | 37 | private void AnalyzeNode(SyntaxNodeAnalysisContext context) 38 | { 39 | // The analyzer will run on every keystroke in the editor, so we are performing the quickest tests first 40 | var member = context.Node as MemberAccessExpressionSyntax; 41 | var identifier = member?.Expression as IdentifierNameSyntax; 42 | 43 | if (identifier == null) 44 | return; 45 | 46 | if (identifier.Identifier.Text != nameof(DateTime)) 47 | return; 48 | 49 | var identifierSymbol = context.SemanticModel.GetSymbolInfo(identifier).Symbol as INamedTypeSymbol; 50 | if (identifierSymbol?.ContainingNamespace.ToString() != nameof(System)) 51 | return; 52 | 53 | var accessor = member.Name.ToString(); 54 | if (accessor != nameof(DateTime.Now)) 55 | return; 56 | 57 | var rule = Rule; 58 | var diagnostic = Diagnostic.Create(rule, member.GetLocation()); 59 | context.ReportDiagnostic(diagnostic); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser/DateTimeNow/DateTimeNowCodeFix.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.CodeActions; 8 | using Microsoft.CodeAnalysis.CodeFixes; 9 | using Microsoft.CodeAnalysis.CSharp; 10 | using Microsoft.CodeAnalysis.CSharp.Syntax; 11 | 12 | namespace DateTimeProviderAnalyser.DateTimeNow 13 | { 14 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DateTimeNowCodeFix)), Shared] 15 | public class DateTimeNowCodeFix : CodeFixProvider 16 | { 17 | public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DateTimeNowAnalyser.DiagnosticId); 18 | 19 | public sealed override FixAllProvider GetFixAllProvider() 20 | { 21 | return WellKnownFixAllProviders.BatchFixer; 22 | } 23 | 24 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 25 | { 26 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 27 | 28 | var diagnostic = context.Diagnostics.First(); 29 | var diagnosticSpan = diagnostic.Location.SourceSpan; 30 | 31 | var expressionSyntax = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First(); 32 | 33 | var codeAction = CodeAction.Create(DateTimeNowAnalyser.Title, cancellationToken => ChangeToDateTimeProvider(context.Document, expressionSyntax, cancellationToken), DateTimeNowAnalyser.Title); 34 | context.RegisterCodeFix(codeAction, diagnostic); 35 | } 36 | 37 | private static async Task ChangeToDateTimeProvider(Document document, SyntaxNode syntaxNode, CancellationToken cancellationToken) 38 | { 39 | var root = await document.GetSyntaxRootAsync(cancellationToken); 40 | var newRoot = root.ReplaceNode(syntaxNode, SyntaxFactory.ParseExpression($"{nameof(DateTimeProvider)}.{nameof(DateTimeProvider.LocalNow)}")); 41 | return document.WithSyntaxRoot(newRoot); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser/DateTimeOffsetNow/DateTimeOffsetNowAnalyser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | 8 | namespace DateTimeProviderAnalyser.DateTimeOffsetNow 9 | { 10 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 11 | public class DateTimeOffsetNowAnalyser : DiagnosticAnalyzer 12 | { 13 | public const string DiagnosticId = nameof(DateTimeOffsetNowAnalyser); 14 | 15 | public const string Title = "Use DateTimeProvider.Now instead of DateTimeOffset"; 16 | public const string MessageFormat = "Use DateTimeProvider.Now instead of DateTimeOffset.Now"; 17 | public const string Description = "Use DateTimeProvider so that date and time is abstracted and easier to test"; 18 | public const string HelpLinkUri = "https://github.com/dennisroche/DateTimeProvider"; 19 | 20 | private const string Category = "Syntax"; 21 | private const bool AlwaysEnabledByDefault = true; 22 | 23 | public DateTimeOffsetNowAnalyser() 24 | { 25 | Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, AlwaysEnabledByDefault, Description, HelpLinkUri); 26 | SupportedDiagnostics = ImmutableArray.Create(Rule); 27 | } 28 | 29 | public DiagnosticDescriptor Rule { get; } 30 | public override ImmutableArray SupportedDiagnostics { get; } 31 | 32 | public override void Initialize(AnalysisContext context) 33 | { 34 | context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.SimpleMemberAccessExpression); 35 | } 36 | 37 | private void AnalyzeNode(SyntaxNodeAnalysisContext context) 38 | { 39 | // The analyzer will run on every keystroke in the editor, so we are performing the quickest tests first 40 | var member = context.Node as MemberAccessExpressionSyntax; 41 | var identifier = member?.Expression as IdentifierNameSyntax; 42 | 43 | if (identifier == null) 44 | return; 45 | 46 | if (identifier.Identifier.Text != nameof(DateTimeOffset)) 47 | return; 48 | 49 | var identifierSymbol = context.SemanticModel.GetSymbolInfo(identifier).Symbol as INamedTypeSymbol; 50 | if (identifierSymbol?.ContainingNamespace.ToString() != nameof(System)) 51 | return; 52 | 53 | var accessor = member.Name.ToString(); 54 | if (accessor != nameof(DateTimeOffset.Now)) 55 | return; 56 | 57 | var rule = Rule; 58 | var diagnostic = Diagnostic.Create(rule, member.GetLocation()); 59 | context.ReportDiagnostic(diagnostic); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser/DateTimeOffsetNow/DateTimeOffsetNowCodeFix.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 DateTimeProviderAnalyser.DateTimeNow; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CodeActions; 9 | using Microsoft.CodeAnalysis.CodeFixes; 10 | using Microsoft.CodeAnalysis.CSharp; 11 | using Microsoft.CodeAnalysis.CSharp.Syntax; 12 | 13 | namespace DateTimeProviderAnalyser.DateTimeOffsetNow 14 | { 15 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DateTimeNowCodeFix)), Shared] 16 | public class DateTimeOffsetNowCodeFix : CodeFixProvider 17 | { 18 | public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DateTimeNowAnalyser.DiagnosticId); 19 | 20 | public sealed override FixAllProvider GetFixAllProvider() 21 | { 22 | return WellKnownFixAllProviders.BatchFixer; 23 | } 24 | 25 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 26 | { 27 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 28 | 29 | var diagnostic = context.Diagnostics.First(); 30 | var diagnosticSpan = diagnostic.Location.SourceSpan; 31 | 32 | var expressionSyntax = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First(); 33 | 34 | var codeAction = CodeAction.Create(DateTimeOffsetNowAnalyser.Title, cancellationToken => ChangeToDateTimeProvider(context.Document, expressionSyntax, cancellationToken), DateTimeOffsetNowAnalyser.Title); 35 | context.RegisterCodeFix(codeAction, diagnostic); 36 | } 37 | 38 | private static async Task ChangeToDateTimeProvider(Document document, SyntaxNode syntaxNode, CancellationToken cancellationToken) 39 | { 40 | var root = await document.GetSyntaxRootAsync(cancellationToken); 41 | var newRoot = root.ReplaceNode(syntaxNode, SyntaxFactory.ParseExpression($"{nameof(DateTimeProvider)}.{nameof(DateTimeProvider.Now)}")); 42 | return document.WithSyntaxRoot(newRoot); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser/DateTimeProviderAnalyser.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | netstandard1.3 6 | $(PackageTargetFallback);portable-net45+win8+wp8+wpa81 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Always 18 | 19 | 20 | Always 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser/DateTimeProviderAnalyser.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | DateTimeProvider.Analyser 5 | $version$ 6 | DateTimeProvider.Analyser 7 | dennisroche 8 | https://github.com/dennisroche/DateTimeProvider/ 9 | false 10 | https://raw.githubusercontent.com/dennisroche/DateTimeProvider/master/LICENSE 11 | Provides Roslyn Analyser to provide code suggestions to change DateTime to DateTimeProvider. 12 | DateTimeAnalyser, DateTimeProvider, analyzers 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | true 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser/DateTimeUtcNow/DateTimeUtcNowAnalyser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | 8 | namespace DateTimeProviderAnalyser.DateTimeUtcNow 9 | { 10 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 11 | public class DateTimeUtcNowAnalyser : DiagnosticAnalyzer 12 | { 13 | public const string DiagnosticId = nameof(DateTimeUtcNowAnalyser); 14 | 15 | public const string Title = "Use DateTimeProvider.UtcNow instead of DateTime"; 16 | public const string MessageFormat = "Use DateTimeProvider.UtcNow instead of DateTime.UtcNow"; 17 | public const string Description = "Use DateTimeProvider so that date and time is abstracted and easier to test"; 18 | public const string HelpLinkUri = "https://github.com/dennisroche/DateTimeProvider"; 19 | 20 | private const string Category = "Syntax"; 21 | private const bool AlwaysEnabledByDefault = true; 22 | 23 | public DateTimeUtcNowAnalyser() 24 | { 25 | Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, AlwaysEnabledByDefault, Description, HelpLinkUri); 26 | SupportedDiagnostics = ImmutableArray.Create(Rule); 27 | } 28 | 29 | public DiagnosticDescriptor Rule { get; } 30 | public override ImmutableArray SupportedDiagnostics { get; } 31 | 32 | public override void Initialize(AnalysisContext context) 33 | { 34 | context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.SimpleMemberAccessExpression); 35 | } 36 | 37 | private void AnalyzeNode(SyntaxNodeAnalysisContext context) 38 | { 39 | // The analyzer will run on every keystroke in the editor, so we are performing the quickest tests first 40 | var member = context.Node as MemberAccessExpressionSyntax; 41 | var identifier = member?.Expression as IdentifierNameSyntax; 42 | 43 | if (identifier == null) 44 | return; 45 | 46 | if (identifier.Identifier.Text != nameof(DateTime)) 47 | return; 48 | 49 | var identifierSymbol = context.SemanticModel.GetSymbolInfo(identifier).Symbol as INamedTypeSymbol; 50 | if (identifierSymbol?.ContainingNamespace.ToString() != nameof(System)) 51 | return; 52 | 53 | var accessor = member.Name.ToString(); 54 | if (accessor != nameof(DateTime.UtcNow)) 55 | return; 56 | 57 | var rule = Rule; 58 | var diagnostic = Diagnostic.Create(rule, member.GetLocation()); 59 | context.ReportDiagnostic(diagnostic); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser/DateTimeUtcNow/DateTimeUtcNowCodeFix.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 DateTimeProviderAnalyser.DateTimeNow; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CodeActions; 9 | using Microsoft.CodeAnalysis.CodeFixes; 10 | using Microsoft.CodeAnalysis.CSharp; 11 | using Microsoft.CodeAnalysis.CSharp.Syntax; 12 | 13 | namespace DateTimeProviderAnalyser.DateTimeUtcNow 14 | { 15 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DateTimeNowCodeFix)), Shared] 16 | public class DateTimeUtcNowCodeFix : CodeFixProvider 17 | { 18 | public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DateTimeNowAnalyser.DiagnosticId); 19 | 20 | public sealed override FixAllProvider GetFixAllProvider() 21 | { 22 | return WellKnownFixAllProviders.BatchFixer; 23 | } 24 | 25 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 26 | { 27 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 28 | 29 | var diagnostic = context.Diagnostics.First(); 30 | var diagnosticSpan = diagnostic.Location.SourceSpan; 31 | 32 | var expressionSyntax = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First(); 33 | 34 | var codeAction = CodeAction.Create(DateTimeUtcNowAnalyser.Title, cancellationToken => ChangeToDateTimeProvider(context.Document, expressionSyntax, cancellationToken), DateTimeUtcNowAnalyser.Title); 35 | context.RegisterCodeFix(codeAction, diagnostic); 36 | } 37 | 38 | private static async Task ChangeToDateTimeProvider(Document document, SyntaxNode syntaxNode, CancellationToken cancellationToken) 39 | { 40 | var root = await document.GetSyntaxRootAsync(cancellationToken); 41 | var newRoot = root.ReplaceNode(syntaxNode, SyntaxFactory.ParseExpression($"{nameof(DateTimeProvider)}.{nameof(DateTimeProvider.UtcNow)}")); 42 | return document.WithSyntaxRoot(newRoot); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser/tools/install.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve 4 | 5 | foreach($analyzersPath in $analyzersPaths) 6 | { 7 | # Install the language agnostic analyzers. 8 | if (Test-Path $analyzersPath) 9 | { 10 | foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) 11 | { 12 | if($project.Object.AnalyzerReferences) 13 | { 14 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 15 | } 16 | } 17 | } 18 | } 19 | 20 | # $project.Type gives the language name like (C# or VB.NET) 21 | $languageFolder = "" 22 | if($project.Type -eq "C#") 23 | { 24 | $languageFolder = "cs" 25 | } 26 | if($project.Type -eq "VB.NET") 27 | { 28 | $languageFolder = "vb" 29 | } 30 | if($languageFolder -eq "") 31 | { 32 | return 33 | } 34 | 35 | foreach($analyzersPath in $analyzersPaths) 36 | { 37 | # Install language specific analyzers. 38 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 39 | if (Test-Path $languageAnalyzersPath) 40 | { 41 | foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll) 42 | { 43 | if($project.Object.AnalyzerReferences) 44 | { 45 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/DateTimeProviderAnalyser/tools/uninstall.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve 4 | 5 | foreach($analyzersPath in $analyzersPaths) 6 | { 7 | # Uninstall the language agnostic analyzers. 8 | if (Test-Path $analyzersPath) 9 | { 10 | foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) 11 | { 12 | if($project.Object.AnalyzerReferences) 13 | { 14 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 15 | } 16 | } 17 | } 18 | } 19 | 20 | # $project.Type gives the language name like (C# or VB.NET) 21 | $languageFolder = "" 22 | if($project.Type -eq "C#") 23 | { 24 | $languageFolder = "cs" 25 | } 26 | if($project.Type -eq "VB.NET") 27 | { 28 | $languageFolder = "vb" 29 | } 30 | if($languageFolder -eq "") 31 | { 32 | return 33 | } 34 | 35 | foreach($analyzersPath in $analyzersPaths) 36 | { 37 | # Uninstall language specific analyzers. 38 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 39 | if (Test-Path $languageAnalyzersPath) 40 | { 41 | foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll) 42 | { 43 | if($project.Object.AnalyzerReferences) 44 | { 45 | try 46 | { 47 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 48 | } 49 | catch 50 | { 51 | 52 | } 53 | } 54 | } 55 | } 56 | } --------------------------------------------------------------------------------