diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/BuiltInDateTimeMethod.cs b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/BuiltInDateTimeMethod.cs new file mode 100644 index 0000000..e788357 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/BuiltInDateTimeMethod.cs @@ -0,0 +1,78 @@ +using ALCops.LinterCop.CodeFixes; +using RoslynTestKit; + +namespace ALCops.LinterCop.Test +{ + public class BuiltInDateTimeMethod : NavCodeAnalysisBase + { + private AnalyzerTestFixture _fixture; + private static readonly Analyzers.BuiltInDateTimeMethod _analyzer = new(); + private string _testCasePath; + + [SetUp] + public void Setup() + { + _fixture = RoslynFixtureFactory.Create(); + + _testCasePath = Path.Combine( + Directory.GetParent( + Environment.CurrentDirectory)!.Parent!.Parent!.FullName, + Path.Combine("Rules", nameof(BuiltInDateTimeMethod))); + } + + [Test] + [TestCase("Date2DMY")] + [TestCase("Date2DWY")] + [TestCase("DT2Date")] + [TestCase("DT2Time")] + [TestCase("FormatHour")] + [TestCase("FormatMillisecond")] + [TestCase("FormatMinute")] + [TestCase("FormatSecond")] + public async Task HasDiagnostic(string testCase) + { + var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasDiagnostic), $"{testCase}.al")) + .ConfigureAwait(false); + + _fixture.HasDiagnosticAtAllMarkers(code, DiagnosticIds.BuiltInDateTimeMethod); + } + + // [Test] + // public async Task NoDiagnostic(string testCase) + // { + // var code = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(NoDiagnostic), $"{testCase}.al")) + // .ConfigureAwait(false); + + // _fixture.NoDiagnosticAtAllMarkers(code, DiagnosticIds.BuiltInDateTimeMethod); + // } + + [Test] + [TestCase("Date2DMY_Day")] + [TestCase("Date2DMY_Month")] + [TestCase("Date2DMY_Year")] + [TestCase("Date2DWY_DayOfWeek")] + [TestCase("Date2DWY_WeekNo")] + [TestCase("DT2Date")] + [TestCase("DT2Time")] + [TestCase("Format_Hour")] + [TestCase("Format_Minute")] + [TestCase("Format_Second")] + [TestCase("Format_Millisecond")] + public async Task HasFix(string testCase) + { + var currentCode = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasFix), testCase, "current.al")) + .ConfigureAwait(false); + + var expectedCode = await File.ReadAllTextAsync(Path.Combine(_testCasePath, nameof(HasFix), testCase, "expected.al")) + .ConfigureAwait(false); + + var fixture = RoslynFixtureFactory.Create( + new CodeFixTestFixtureConfig + { + AdditionalAnalyzers = [_analyzer] + }); + + fixture.TestCodeFix(currentCode, expectedCode, DiagnosticDescriptors.BuiltInDateTimeMethod); + } + } +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/DT2Date.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/DT2Date.al new file mode 100644 index 0000000..dea0970 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/DT2Date.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDateTime: DateTime; + MyDate: Date; + begin + MyDate := [|DT2Date(MyDateTime)|]; + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/DT2Time.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/DT2Time.al new file mode 100644 index 0000000..cc7cbd9 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/DT2Time.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDateTime: DateTime; + MyTime: Time; + begin + MyTime := [|DT2Time(MyDateTime)|]; + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/Date2DMY.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/Date2DMY.al new file mode 100644 index 0000000..e96b3d3 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/Date2DMY.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + MyInteger: Integer; + begin + MyInteger := [|Date2DMY(MyDate, 1)|]; + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/Date2DWY.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/Date2DWY.al new file mode 100644 index 0000000..e96b3d3 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/Date2DWY.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + MyInteger: Integer; + begin + MyInteger := [|Date2DMY(MyDate, 1)|]; + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/FormatHour.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/FormatHour.al new file mode 100644 index 0000000..52e8ff7 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/FormatHour.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + i: Integer; + begin + Evaluate(i, [|Format(MyTime, 2, '')|]); + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/FormatMillisecond.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/FormatMillisecond.al new file mode 100644 index 0000000..99ff68e --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/FormatMillisecond.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + i: Integer; + begin + Evaluate(i, [|Format(MyTime, 2, '')|]); + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/FormatMinute.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/FormatMinute.al new file mode 100644 index 0000000..143e357 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/FormatMinute.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + i: Integer; + begin + Evaluate(i, [|Format(MyTime, 2, '')|]); + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/FormatSecond.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/FormatSecond.al new file mode 100644 index 0000000..bc3062e --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasDiagnostic/FormatSecond.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + i: Integer; + begin + Evaluate(i, [|Format(MyTime, 2, '')|]); + end; +} \ No newline at end of file diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/DT2Date/current.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/DT2Date/current.al new file mode 100644 index 0000000..0ff182d --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/DT2Date/current.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDateTime: DateTime; + DateValue: Date; + begin + DateValue := [|DT2Date(MyDateTime)|]; + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/DT2Date/expected.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/DT2Date/expected.al new file mode 100644 index 0000000..af68eb6 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/DT2Date/expected.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDateTime: DateTime; + DateValue: Date; + begin + DateValue := MyDateTime.Date(); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/DT2Time/current.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/DT2Time/current.al new file mode 100644 index 0000000..ea2a5f8 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/DT2Time/current.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDateTime: DateTime; + TimeValue: Time; + begin + TimeValue := [|DT2Time(MyDateTime)|]; + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/DT2Time/expected.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/DT2Time/expected.al new file mode 100644 index 0000000..120d3f1 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/DT2Time/expected.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDateTime: DateTime; + TimeValue: Time; + begin + TimeValue := MyDateTime.Time(); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Day/current.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Day/current.al new file mode 100644 index 0000000..94ab5ab --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Day/current.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + DayValue: Integer; + begin + DayValue := [|Date2DMY(MyDate, 1)|]; + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Day/expected.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Day/expected.al new file mode 100644 index 0000000..84f1766 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Day/expected.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + DayValue: Integer; + begin + DayValue := MyDate.Day(); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Month/current.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Month/current.al new file mode 100644 index 0000000..cb2366c --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Month/current.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + MonthValue: Integer; + begin + MonthValue := [|Date2DMY(MyDate, 2)|]; + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Month/expected.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Month/expected.al new file mode 100644 index 0000000..4abf771 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Month/expected.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + MonthValue: Integer; + begin + MonthValue := MyDate.Month(); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Year/current.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Year/current.al new file mode 100644 index 0000000..1c13c7f --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Year/current.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + YearValue: Integer; + begin + YearValue := [|Date2DMY(MyDate, 3)|]; + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Year/expected.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Year/expected.al new file mode 100644 index 0000000..ed4876c --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DMY_Year/expected.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + YearValue: Integer; + begin + YearValue := MyDate.Year(); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DWY_DayOfWeek/current.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DWY_DayOfWeek/current.al new file mode 100644 index 0000000..1f17f0c --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DWY_DayOfWeek/current.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + DayOfWeekValue: Integer; + begin + DayOfWeekValue := [|Date2DWY(MyDate, 1)|]; + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DWY_DayOfWeek/expected.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DWY_DayOfWeek/expected.al new file mode 100644 index 0000000..61b8500 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DWY_DayOfWeek/expected.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + DayOfWeekValue: Integer; + begin + DayOfWeekValue := MyDate.DayOfWeek(); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DWY_WeekNo/current.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DWY_WeekNo/current.al new file mode 100644 index 0000000..c8de4ed --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DWY_WeekNo/current.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + WeekNoValue: Integer; + begin + WeekNoValue := [|Date2DWY(MyDate, 2)|]; + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DWY_WeekNo/expected.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DWY_WeekNo/expected.al new file mode 100644 index 0000000..6e57687 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Date2DWY_WeekNo/expected.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyDate: Date; + WeekNoValue: Integer; + begin + WeekNoValue := MyDate.WeekNo(); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Hour/current.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Hour/current.al new file mode 100644 index 0000000..ada35f4 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Hour/current.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + HourValue: Integer; + begin + Evaluate(HourValue, [|Format(MyTime, 2, '')|]); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Hour/expected.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Hour/expected.al new file mode 100644 index 0000000..3c303d6 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Hour/expected.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + HourValue: Integer; + begin + Evaluate(HourValue, MyTime.Hour()); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Millisecond/current.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Millisecond/current.al new file mode 100644 index 0000000..51e5a8d --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Millisecond/current.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + MillisecondValue: Integer; + begin + Evaluate(MillisecondValue, [|Format(MyTime, 3, '')|]); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Millisecond/expected.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Millisecond/expected.al new file mode 100644 index 0000000..9704a9f --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Millisecond/expected.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + MillisecondValue: Integer; + begin + Evaluate(MillisecondValue, MyTime.Millisecond()); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Minute/current.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Minute/current.al new file mode 100644 index 0000000..598faaa --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Minute/current.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + MinuteValue: Integer; + begin + Evaluate(MinuteValue, [|Format(MyTime, 2, '')|]); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Minute/expected.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Minute/expected.al new file mode 100644 index 0000000..00f9547 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Minute/expected.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + MinuteValue: Integer; + begin + Evaluate(MinuteValue, MyTime.Minute()); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Second/current.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Second/current.al new file mode 100644 index 0000000..1e564e0 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Second/current.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + SecondValue: Integer; + begin + Evaluate(SecondValue, [|Format(MyTime, 2, '')|]); + end; +} diff --git a/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Second/expected.al b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Second/expected.al new file mode 100644 index 0000000..3efab82 --- /dev/null +++ b/src/ALCops.LinterCop.Test/Rules/BuiltInDateTimeMethod/HasFix/Format_Second/expected.al @@ -0,0 +1,10 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + MyTime: Time; + SecondValue: Integer; + begin + Evaluate(SecondValue, MyTime.Second()); + end; +} diff --git a/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.cs b/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.cs index 342208b..3628179 100644 --- a/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.cs +++ b/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.cs @@ -147,6 +147,42 @@ internal static string AppManifestRuntimeBehindTitle { } } + /// + /// Looks up a localized string similar to APClops: Use modern Date/Time/DateTime method. + /// + internal static string BuiltInDateTimeMethodCodeAction { + get { + return ResourceManager.GetString("BuiltInDateTimeMethodCodeAction", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace outdated functions for extracting specific parts of Date, Time, and DateTime types (such as day, month, hour, or second) with the new, modernized methods.. + /// + internal static string BuiltInDateTimeMethodDescription { + get { + return ResourceManager.GetString("BuiltInDateTimeMethodDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the new method {0}.{1} to extract specific parts of date/time values.. + /// + internal static string BuiltInDateTimeMethodMessageFormat { + get { + return ResourceManager.GetString("BuiltInDateTimeMethodMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use new Date/Time/DateTime methods for extracting parts. + /// + internal static string BuiltInDateTimeMethodTitle { + get { + return ResourceManager.GetString("BuiltInDateTimeMethodTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Reports an individual Cognitive Complexity increment at the exact source location where complexity increases. Each diagnostic explains how the increment is composed, including the base increment and any additional nesting penalty. This diagnostic is intended to make the complexity calculation transparent and explainable.. /// diff --git a/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.resx b/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.resx index 4a4c547..c0d50ae 100644 --- a/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.resx +++ b/src/ALCops.LinterCop/ALCops.LinterCopAnalyzers.resx @@ -147,6 +147,18 @@ Specifying a runtime version in app.json is optional. When a runtime is explicitly defined and falls behind the targeted application or platform, newer language features and runtime capabilities may be unavailable, leading to unnecessary limitations or inconsistencies. + + Use new Date/Time/DateTime methods for extracting parts + + + Use the new method {0}.{1} to extract specific parts of date/time values. + + + Replace outdated functions for extracting specific parts of Date, Time, and DateTime types (such as day, month, hour, or second) with the new, modernized methods. + + + ALCops: Use modern Date/Time/DateTime method + Cognitive complexity metric diff --git a/src/ALCops.LinterCop/Analyzers/BuiltInDateTimeMethod.cs b/src/ALCops.LinterCop/Analyzers/BuiltInDateTimeMethod.cs new file mode 100644 index 0000000..022e76a --- /dev/null +++ b/src/ALCops.LinterCop/Analyzers/BuiltInDateTimeMethod.cs @@ -0,0 +1,111 @@ +using System.Collections.Immutable; +using ALCops.Common.Extensions; +using ALCops.Common.Reflection; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; + +namespace ALCops.LinterCop.Analyzers; + +[DiagnosticAnalyzer] +public sealed class BuiltInDateTimeMethod : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create( + DiagnosticDescriptors.BuiltInDateTimeMethod); + + public override VersionCompatibility SupportedVersions => + VersionProvider.VersionCompatibility.Fall2024OrGreater; + + public override void Initialize(AnalysisContext context) => + context.RegisterOperationAction( + AnalyzeInvocation, + EnumProvider.OperationKind.InvocationExpression); + + private void AnalyzeInvocation(OperationAnalysisContext ctx) + { + if (ctx.IsObsolete() || ctx.Operation is not IInvocationExpression operation) + return; + + if (operation.TargetMethod.MethodKind != EnumProvider.MethodKind.BuiltInMethod || + operation.Arguments.Length < 1) + return; + + if (operation.Syntax is not InvocationExpressionSyntax invocationExpression) + return; + + InvocationExpressionSyntax? replacementMethod = GetReplacementMethod(operation.TargetMethod.Name, invocationExpression); + if (replacementMethod is null) + return; + + var properties = ImmutableDictionary.Empty + .Add("ReplacementMethodName", replacementMethod.Expression.ToString()); + + ctx.ReportDiagnostic( + Diagnostic.Create( + DiagnosticDescriptors.BuiltInDateTimeMethod, + invocationExpression.GetLocation(), + properties, + operation.Arguments[0].Value.Syntax.ToString(), + replacementMethod.ToString())); + } + + private static InvocationExpressionSyntax? GetReplacementMethod(string methodName, InvocationExpressionSyntax invocationExpression) + { + var arguments = invocationExpression.ArgumentList.Arguments; + + return methodName switch + { + "Date2DMY" when arguments.Count == 2 => GetDate2DMYReplacement(arguments[1]), + "Date2DWY" when arguments.Count == 2 => GetDate2DWYReplacement(arguments[1]), + "DT2Date" => GetInvocationExpressionSyntax("Date"), + "DT2Time" => GetInvocationExpressionSyntax("Time"), + "Format" when arguments.Count == 3 => GetFormatReplacement(arguments[2]), + _ => null + }; + } + + private static InvocationExpressionSyntax? GetDate2DMYReplacement(CodeExpressionSyntax argument) + { + return GetIntegerValueOrNull(argument) switch + { + 1 => GetInvocationExpressionSyntax("Day"), + 2 => GetInvocationExpressionSyntax("Month"), + 3 => GetInvocationExpressionSyntax("Year"), + _ => null + }; + } + + private static InvocationExpressionSyntax? GetDate2DWYReplacement(CodeExpressionSyntax argument) + { + return GetIntegerValueOrNull(argument) switch + { + 1 => GetInvocationExpressionSyntax("DayOfWeek"), + 2 => GetInvocationExpressionSyntax("WeekNo"), + 3 => null, // Year() isn't returning the same value. When the input date to the Date2DWY method is in a week that spans two years, the Date2DWY method computes the output year as the year that has the most days. + _ => null + }; + } + + private static InvocationExpressionSyntax? GetFormatReplacement(CodeExpressionSyntax argument) + { + return argument.GetIdentifierOrLiteralValue() switch + { + "" => GetInvocationExpressionSyntax("Hour"), + "" => GetInvocationExpressionSyntax("Minute"), + "" => GetInvocationExpressionSyntax("Second"), + "" => GetInvocationExpressionSyntax("Millisecond"), + _ => null + }; + } + + private static int? GetIntegerValueOrNull(CodeExpressionSyntax argument) + => int.TryParse(argument.GetIdentifierOrLiteralValue(), out var value) ? value : null; + + private static InvocationExpressionSyntax GetInvocationExpressionSyntax(string identifier) + { + return SyntaxFactory.InvocationExpression( + SyntaxFactory.IdentifierName(identifier), + SyntaxFactory.ArgumentList()); + } +} \ No newline at end of file diff --git a/src/ALCops.LinterCop/CodeFixes/BuiltInDateTimeMethod.cs b/src/ALCops.LinterCop/CodeFixes/BuiltInDateTimeMethod.cs new file mode 100644 index 0000000..b898260 --- /dev/null +++ b/src/ALCops.LinterCop/CodeFixes/BuiltInDateTimeMethod.cs @@ -0,0 +1,135 @@ +using System.Collections.Immutable; +using Microsoft.Dynamics.Nav.CodeAnalysis; +using Microsoft.Dynamics.Nav.CodeAnalysis.CodeActions; +using Microsoft.Dynamics.Nav.CodeAnalysis.CodeActions.Mef; +using Microsoft.Dynamics.Nav.CodeAnalysis.CodeFixes; +using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax; +using Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces; + +namespace ALCops.LinterCop.CodeFixes; + +[CodeFixProvider(nameof(BuiltInDateTimeMethodCodeFixProvider))] +public sealed class BuiltInDateTimeMethodCodeFixProvider : CodeFixProvider +{ +#if NETSTANDARD2_1 + // C# 9 records require 'System.Runtime.CompilerServices.IsExternalInit' which doesn't exist in netstandard2.1. + // We use a regular class for netstandard2.1 and a record for .NET 8+ to maintain compatibility with both targets. + private sealed class CodeFixProperties + { + public string ReplacementMethodName { get; } + + private CodeFixProperties(string _replacementMethodName) + { + ReplacementMethodName = _replacementMethodName; + } + + public static CodeFixProperties? TryParse(ImmutableDictionary? properties) + { + if (properties is null) + return null; + + if (!properties.TryGetValue(nameof(ReplacementMethodName), out var _replacementMethodName) || string.IsNullOrEmpty(_replacementMethodName)) + return null; + + return new CodeFixProperties(_replacementMethodName); + } + } +#endif + +#if NET8_0_OR_GREATER + private sealed record CodeFixProperties(string ReplacementMethodName) + { + public static CodeFixProperties? TryParse(ImmutableDictionary? properties) + { + if (properties is null) + return null; + + if (!properties.TryGetValue(nameof(ReplacementMethodName), out var _replacementMethodName) || string.IsNullOrEmpty(_replacementMethodName)) + return null; + + return new CodeFixProperties(_replacementMethodName); + } + } +#endif + + private class BuiltInDateTimeMethodCodeAction : CodeAction.DocumentChangeAction + { + public override CodeActionKind Kind => CodeActionKind.QuickFix; + public override bool SupportsFixAll { get; } + public override string? FixAllSingleInstanceTitle => string.Empty; + public override string? FixAllTitle => Title; + + public BuiltInDateTimeMethodCodeAction(string title, + Func> createChangedDocument, string equivalenceKey, bool generateFixAll) + : base(title, createChangedDocument, equivalenceKey) + { + SupportsFixAll = generateFixAll; + } + } + + public override ImmutableArray FixableDiagnosticIds => + ImmutableArray.Create(DiagnosticDescriptors.BuiltInDateTimeMethod.Id); + + public sealed override FixAllProvider GetFixAllProvider() => + WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext ctx) + { + Document document = ctx.Document; + TextSpan span = ctx.Span; + CancellationToken cancellationToken = ctx.CancellationToken; + + SyntaxNode syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + RegisterInstanceCodeFix(ctx, syntaxRoot, span, document); + } + + private static void RegisterInstanceCodeFix(CodeFixContext ctx, SyntaxNode syntaxRoot, TextSpan span, Document document) + { + SyntaxNode node = syntaxRoot.FindNode(span); + + var diagnostic = ctx.Diagnostics + .FirstOrDefault(d => d.Id == DiagnosticDescriptors.BuiltInDateTimeMethod.Id); + + var properties = CodeFixProperties.TryParse(diagnostic?.Properties); + if (properties is null) + return; + + ctx.RegisterCodeFix(CreateCodeAction(node, document, properties, generateFixAll: true), ctx.Diagnostics[0]); + } + + private static BuiltInDateTimeMethodCodeAction CreateCodeAction(SyntaxNode node, Document document, CodeFixProperties properties, bool generateFixAll) + { + return new BuiltInDateTimeMethodCodeAction( + LinterCopAnalyzers.BuiltInDateTimeMethodCodeAction, + ct => ReplaceWithNewMethodAsync(document, node, properties, ct), + nameof(ObjectIdInDeclarationCodeFixProvider), + generateFixAll); + } + + private static async Task ReplaceWithNewMethodAsync(Document document, SyntaxNode node, CodeFixProperties properties, CancellationToken cancellationToken) + { + Task syntaxRootTask = document.GetSyntaxRootAsync(cancellationToken); + + if (node is not InvocationExpressionSyntax invocation) + return document; + + if (invocation.ArgumentList.Arguments.Count == 0) + return document; + + var firstArgument = invocation.ArgumentList.Arguments[0]; + + var memberAccess = SyntaxFactory.MemberAccessExpression( + firstArgument, + properties.ReplacementMethodName); + + var newInvocation = SyntaxFactory.InvocationExpression(memberAccess) + .WithTriviaFrom(invocation); + + var root = await syntaxRootTask.ConfigureAwait(false); + if (root is null) + return document; + + var newRoot = root.ReplaceNode(invocation, newInvocation); + return document.WithSyntaxRoot(newRoot); + } +} \ No newline at end of file diff --git a/src/ALCops.LinterCop/DiagnosticDescriptors.cs b/src/ALCops.LinterCop/DiagnosticDescriptors.cs index 148c0d6..acdf79e 100644 --- a/src/ALCops.LinterCop/DiagnosticDescriptors.cs +++ b/src/ALCops.LinterCop/DiagnosticDescriptors.cs @@ -35,6 +35,16 @@ public static class DiagnosticDescriptors description: LinterCopAnalyzers.AppManifestRuntimeBehindDescription, helpLinkUri: GetHelpUri(DiagnosticIds.AppManifestRuntimeBehind)); + public static readonly DiagnosticDescriptor BuiltInDateTimeMethod = new( + id: DiagnosticIds.BuiltInDateTimeMethod, + title: LinterCopAnalyzers.BuiltInDateTimeMethodMessageFormat, + messageFormat: LinterCopAnalyzers.BuiltInDateTimeMethodMessageFormat, + category: Category.Design, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: LinterCopAnalyzers.BuiltInDateTimeMethodDescription, + helpLinkUri: GetHelpUri(DiagnosticIds.BuiltInDateTimeMethod)); + public static readonly DiagnosticDescriptor CognitiveComplexityMetric = new( id: DiagnosticIds.CognitiveComplexityMetric, title: LinterCopAnalyzers.CognitiveComplexityMetricMessageFormat, diff --git a/src/ALCops.LinterCop/DiagnosticIds.cs b/src/ALCops.LinterCop/DiagnosticIds.cs index 58043b5..274ad24 100644 --- a/src/ALCops.LinterCop/DiagnosticIds.cs +++ b/src/ALCops.LinterCop/DiagnosticIds.cs @@ -21,6 +21,7 @@ public static class DiagnosticIds public static readonly string ApiPageCanonicalFieldNameGuide = "LC0063"; public static readonly string UseIsEmptyMethodInsteadOfCount = "LC0081"; public static readonly string UseQueryOrFindWithNextInsteadOfCount = "LC0082"; + public static readonly string BuiltInDateTimeMethod = "LC0083"; public static readonly string PageStyleStringLiteral = "LC0086"; public static readonly string CognitiveComplexityMetric = "LC0089"; public static readonly string CognitiveComplexityIncrement = "LC0089i";