├── .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 [](https://ci.appveyor.com/project/dennisroche/datetimeprovider) [](https://www.nuget.org/packages/DateTimeProvider/) [](https://github.com/dotnet/standard/blob/master/docs/versions/netstandard1.3.md)  [](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 [](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 | }
--------------------------------------------------------------------------------