diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index 22aa4f5b3f..9291dfffe7 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -26,5 +26,6 @@ assertj_version=3.15.0 rd_version=0.203.161 rider_nuget_sdk_version=2020.3.0-eap07 +rider_fsharp_plugin_version=2020.3.4646 rider_test_local_env_run=true diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/ReSharper.Azure.sln b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/ReSharper.Azure.sln index 64ab8f2e67..850f5b511f 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/ReSharper.Azure.sln +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/ReSharper.Azure.sln @@ -14,6 +14,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Psi", "src\Azure.Psi\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Intellisense", "src\Azure.Intellisense\Azure.Intellisense.csproj", "{D6136977-43A4-49F4-92D9-72FD0CB30603}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.FSharp", "src\Azure.FSharp\Azure.FSharp.csproj", "{DD9BFB34-980E-4514-8FFA-79F9BE9BF756}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -40,6 +42,10 @@ Global {D6136977-43A4-49F4-92D9-72FD0CB30603}.Debug|Any CPU.Build.0 = Debug|Any CPU {D6136977-43A4-49F4-92D9-72FD0CB30603}.Release|Any CPU.ActiveCfg = Release|Any CPU {D6136977-43A4-49F4-92D9-72FD0CB30603}.Release|Any CPU.Build.0 = Release|Any CPU + {DD9BFB34-980E-4514-8FFA-79F9BE9BF756}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD9BFB34-980E-4514-8FFA-79F9BE9BF756}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD9BFB34-980E-4514-8FFA-79F9BE9BF756}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD9BFB34-980E-4514-8FFA-79F9BE9BF756}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {31AE4101-68C2-407C-B780-5F9CAD5F6203} = {5E7A51CC-C345-433D-B91B-BE1B9BCAD6BB} @@ -47,5 +53,6 @@ Global {ECB68A22-1792-4EAB-960B-DC421DFB17A3} = {5E7A51CC-C345-433D-B91B-BE1B9BCAD6BB} {060E2258-BCFF-4616-AAE4-A9CA7E5FC744} = {5E7A51CC-C345-433D-B91B-BE1B9BCAD6BB} {D6136977-43A4-49F4-92D9-72FD0CB30603} = {5E7A51CC-C345-433D-B91B-BE1B9BCAD6BB} + {DD9BFB34-980E-4514-8FFA-79F9BE9BF756} = {5E7A51CC-C345-433D-B91B-BE1B9BCAD6BB} EndGlobalSection EndGlobal diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.Daemon/Errors/FunctionAppErrors.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.Daemon/Errors/FunctionAppErrors.xml index 24c2590f4d..273865a18b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.Daemon/Errors/FunctionAppErrors.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.Daemon/Errors/FunctionAppErrors.xml @@ -68,7 +68,7 @@ --> + configurableSeverity="Azure.CSharp.FunctionApp.TimerTriggerCronExpression"> diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/Azure.FSharp.csproj b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/Azure.FSharp.csproj new file mode 100644 index 0000000000..8e34c33fe9 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/Azure.FSharp.csproj @@ -0,0 +1,45 @@ + + + + net461 + JetBrains.ReSharper.Azure.FSharp + JetBrains.ReSharper.Azure.FSharp + + + + + + + + + + + + + + + ..\..\..\build\dependencies\dotnet\FSharp.Compiler.Service.dll + + + ..\..\..\build\dependencies\dotnet\JetBrains.ReSharper.Plugins.FSharp.Common.dll + + + ..\..\..\build\dependencies\dotnet\JetBrains.ReSharper.Plugins.FSharp.ProjectModelBase.dll + + + ..\..\..\build\dependencies\dotnet\JetBrains.ReSharper.Plugins.FSharp.Psi.dll + + + ..\..\..\build\dependencies\dotnet\JetBrains.ReSharper.Plugins.FSharp.Psi.Features.dll + + + + + + JetBrains.ReSharper.Azure.FSharp.Errors.FunctionAppErrors + Errors\FunctionAppErrors.Generated.cs + ERRORS + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/Errors/FunctionAppErrors.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/Errors/FunctionAppErrors.xml new file mode 100644 index 0000000000..eb8ba35ae7 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/Errors/FunctionAppErrors.xml @@ -0,0 +1,82 @@ + + + + + JetBrains.ReSharper.Azure.Daemon.Highlighters; + JetBrains.ReSharper.Azure.FSharp.Errors; + JetBrains.ReSharper.Azure.FSharp.FunctionApp.Stages.Analysis; + JetBrains.ReSharper.Plugins.FSharp.Psi; + JetBrains.ReSharper.Plugins.FSharp.Psi.Tree; + + + + + + + + + + + + + + + Invalid Function App Timer Trigger Cron expression + Function App Timer Trigger Cron expresion is not valid and can not be used. + + + + + + + + + + + + cronErrorMessage + + Expression.GetHighlightingRange() + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/FunctionApp/CodeCompletion/Rules/FSharpTimerTriggerCronArgumentsProvider.cs b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/FunctionApp/CodeCompletion/Rules/FSharpTimerTriggerCronArgumentsProvider.cs new file mode 100644 index 0000000000..c99715a255 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/FunctionApp/CodeCompletion/Rules/FSharpTimerTriggerCronArgumentsProvider.cs @@ -0,0 +1,145 @@ +// Copyright (c) 2020 JetBrains s.r.o. +//

+// All rights reserved. +//

+// MIT License +//

+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +//

+// The above copyright notice and this permission notice shall be included in all copies or substantial portions of +// the Software. +//

+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using JetBrains.Annotations; +using JetBrains.DocumentModel; +using JetBrains.ReSharper.Azure.Psi.FunctionApp; +using JetBrains.ReSharper.Feature.Services.CodeCompletion; +using JetBrains.ReSharper.Feature.Services.CodeCompletion.Infrastructure; +using JetBrains.ReSharper.Feature.Services.CodeCompletion.Infrastructure.LookupItems; +using JetBrains.ReSharper.Features.Intellisense.CodeCompletion.CSharp; +using JetBrains.ReSharper.Plugins.FSharp.Psi; +using JetBrains.ReSharper.Plugins.FSharp.Psi.Features.CodeCompletion; +using JetBrains.ReSharper.Plugins.FSharp.Psi.Impl; +using JetBrains.ReSharper.Plugins.FSharp.Psi.Tree; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Tree; + +namespace JetBrains.ReSharper.Azure.FSharp.FunctionApp.CodeCompletion.Rules +{ + [Language(typeof(FSharpLanguage))] + public class FSharpTimerTriggerCronArgumentsProvider : ItemsProviderOfSpecificContext + { + // TODO: Refactor so C# and F# share this + private readonly struct CronSuggestion + { + public readonly string Expression; + public readonly string Description; + + public CronSuggestion(string expression, string description) + { + Expression = expression; + Description = description; + } + } + + // TODO: Refactor so C# and F# share this + private readonly CronSuggestion[] CronSuggestions = + { + new CronSuggestion("* * * * * *", "Every second"), + new CronSuggestion("0 * * * * *", "Every minute"), + new CronSuggestion("0 */5 * * * *", "Every 5 minutes"), + new CronSuggestion("0 0 * * * *", "Every hour"), + new CronSuggestion("0 0 */6 * * *", "Every 6 hours at minute 0"), + new CronSuggestion("0 0 8-18 * * *", "Every hour between 08:00 AM and 06:59 PM"), + new CronSuggestion("0 0 0 * * *", "At 12:00 AM"), + new CronSuggestion("0 0 10 * * *", "At 10:00 AM"), + new CronSuggestion("0 0 * * * 1-5", "Every hour, Monday through Friday"), + new CronSuggestion("0 0 0 * * 0", "At 12:00 AM, only on Sunday"), + new CronSuggestion("0 0 9 * * Mon", "At 09:00 AM, only on Monday"), + new CronSuggestion("0 0 0 1 * *", "At 12:00 AM, on day 1 of the month"), + new CronSuggestion("0 0 0 1 1 *", "At 12:00 AM, on day 1 of the month, only in January"), + new CronSuggestion("0 0 * * * Sun", "Every hour, only on Sunday"), + new CronSuggestion("0 0 0 * * Sat,Sun", "At 12:00 AM, only on Saturday and Sunday"), + new CronSuggestion("0 0 0 * * 6,0", "At 12:00 AM, only on Saturday and Sunday"), + new CronSuggestion("0 0 0 1-7 * Sun", "At 12:00 AM, between day 1 and 7 of the month, only on Sunday"), + new CronSuggestion("11 5 23 * * *", "At 11:05:11 PM"), + new CronSuggestion("*/15 * * * * *", "Every 15 seconds"), + new CronSuggestion("0 30 9 * Jan Mon", "At 09:30 AM, only on Monday, only in January") + }; + + protected override bool IsAvailable(FSharpCodeCompletionContext context) + { + return IsStringLiteral(context) && + IsTimerTriggerAnnotation(context) && + context.BasicContext.CodeCompletionType == CodeCompletionType.BasicCompletion; + } + + protected override bool AddLookupItems(FSharpCodeCompletionContext context, IItemsCollector collector) + { + foreach (var cronSuggestion in CronSuggestions) + { + var token = context.TokenAtCaret as ITokenNode; + if (token == null) return false; + + var literalExpression = token.Parent as ILiteralExpr; + if (literalExpression == null) return false; + + var ranges = GetRangeFor(literalExpression); + + var lookupItem = + CSharpLookupItemFactory.Instance.CreateTextLookupItem(new TextLookupRanges(ranges, ranges), + "\"" + cronSuggestion.Expression + "\""); + + lookupItem.Presentation.DisplayName.Text = cronSuggestion.Expression; + lookupItem.Presentation.DisplayTypeName.Text = cronSuggestion.Description; + + // Replace spaces prefixes with nbsp to make sure terminates the lookup. + // It is required since we need to support case when a user choose to complete by . + // TODO: Disable until RIDER-45943 is fixed. + // lookupItem.WithMatcher(item => + // new TextualMatcher(item.Info.Text.Replace(" ", "·"), item.Info)); + + collector.Add(lookupItem); + } + + return true; + } + + private static DocumentRange GetRangeFor([NotNull] ILiteralExpr expression) + { + var underQuotesRange = expression.GetTreeTextRange(); + var containingFile = expression.GetContainingFile(); + return containingFile?.GetDocumentRange(underQuotesRange) ?? DocumentRange.InvalidRange; + } + + private bool IsStringLiteral([NotNull] FSharpCodeCompletionContext context) + { + return context.TokenAtCaret is ITokenNode token && + token.GetTokenType().IsStringLiteral; + } + + private bool IsTimerTriggerAnnotation([NotNull] FSharpCodeCompletionContext context) + { + var token = context.TokenAtCaret as ITokenNode; + if (token == null) return false; + + var literalExpression = token.Parent as ILiteralExpr; + var attribute = AttributeNavigator.GetByExpression(literalExpression.IgnoreParentParens()); + + if (attribute == null) return false; + if (attribute.Arguments.Count != 1) return false; + + var resolveResult = attribute.ReferenceName.Reference.Resolve(); + return resolveResult.DeclaredElement is ITypeElement typeElement && + FunctionAppFinder.IsTimerTriggerAttribute(typeElement); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/FunctionApp/Stages/Analysis/TimerTriggerCronProblemAnalyzer.cs b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/FunctionApp/Stages/Analysis/TimerTriggerCronProblemAnalyzer.cs new file mode 100644 index 0000000000..c71fd14b34 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/FunctionApp/Stages/Analysis/TimerTriggerCronProblemAnalyzer.cs @@ -0,0 +1,85 @@ +// Copyright (c) 2020 JetBrains s.r.o. +// +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of +// the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using JetBrains.ReSharper.Azure.FSharp.Errors.FunctionAppErrors; +using JetBrains.ReSharper.Azure.Psi.FunctionApp; +using JetBrains.ReSharper.Feature.Services.Daemon; +using JetBrains.ReSharper.Plugins.FSharp.Psi.Impl.Tree; +using JetBrains.ReSharper.Plugins.FSharp.Psi.Tree; +using JetBrains.ReSharper.Psi; +using JetBrains.Util; +using NCrontab; + +namespace JetBrains.ReSharper.Azure.FSharp.FunctionApp.Stages.Analysis +{ + ///

+ /// Analyzer for Cron expressions in Function App Timer Trigger matching NCRONTAB specification. + /// https://github.com/atifaziz/NCrontab + /// + /// In general, NCRONTAB expressions are as follows: + /// + /// * * * * * * + /// - - - - - - + /// | | | | | | + /// | | | | | +--- day of week (0 - 6) (Sunday=0) + /// | | | | +----- month (1 - 12) + /// | | | +------- day of month (1 - 31) + /// | | +--------- hour (0 - 23) + /// | +----------- min (0 - 59) + /// +------------- sec (0 - 59) + /// + [ElementProblemAnalyzer(typeof(IAttribute), HighlightingTypes = new[] + { + typeof(TimerTriggerCronExpressionError) + })] + public class TimerTriggerCronProblemAnalyzer : ElementProblemAnalyzer + { + protected override void Run(IAttribute element, ElementProblemAnalyzerData data, IHighlightingConsumer consumer) + { + if (element.Arguments.Count != 1) return; + + var resolveResult = element.ReferenceName.Reference.Resolve(); + var typeElement = resolveResult.DeclaredElement as ITypeElement; + if (typeElement == null) return; + + if (!FunctionAppFinder.IsTimerTriggerAttribute(typeElement)) return; + + var expressionArgument = element.Arguments[0].Expression; + if (expressionArgument == null || !expressionArgument.Type().IsString()) return; + + var fSharpString = (expressionArgument as ILiteralExpr)?.Literal as FSharpString; + var literal = fSharpString?.ConstantValue.Value as string; + if (literal.IsEmpty()) return; + + literal = literal.Trim('"'); + + if (literal.StartsWith("%") && literal.EndsWith("%") && literal.Length > 2) return; + + try + { + CrontabSchedule.Parse(literal, new CrontabSchedule.ParseOptions {IncludingSeconds = true}); + } + catch (CrontabException e) + { + consumer.AddHighlighting(new TimerTriggerCronExpressionError(expressionArgument, e.Message)); + } + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/ZoneMarker.cs b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/ZoneMarker.cs new file mode 100644 index 0000000000..f3ee3670e8 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/ReSharper.Azure/src/Azure.FSharp/ZoneMarker.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2020 JetBrains s.r.o. +//

+// All rights reserved. +//

+// MIT License +//

+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +//

+// The above copyright notice and this permission notice shall be included in all copies or substantial portions of +// the Software. +//

+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using JetBrains.Application.BuildScript.Application.Zones; +using JetBrains.ReSharper.Plugins.FSharp; + +namespace JetBrains.ReSharper.Azure.FSharp +{ + [ZoneMarker] + public class ZoneMarker : IRequire + { + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/build.gradle b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/build.gradle index 2440338de2..c875d51dce 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/build.gradle +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/build.gradle @@ -43,7 +43,17 @@ intellij { // Workaround for https://youtrack.jetbrains.com/issue/IDEA-179607 def extraPlugins = [ "rider-plugins-appender" ] - plugins = ['DatabaseTools', 'JavaScriptLanguage', 'terminal', 'restClient'] + extraPlugins + plugins = ['DatabaseTools', 'JavaScriptLanguage', 'terminal', 'restClient', "com.jetbrains.rider.fsharp:$rider_fsharp_plugin_version"] + extraPlugins + + // HACK: -- Load F# plugin from custom repository (our local disk) + pluginsRepo { + marketplace() + + def url = new File("${buildDir}/dependencies").toURI().toURL().toString() + println(url) + custom(url) + } + // /-- downloadSources = Boolean.valueOf(sources) } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/resources/META-INF/plugin.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/resources/META-INF/plugin.xml index 5502c4a838..c4479e9f01 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/rider/resources/META-INF/plugin.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/rider/resources/META-INF/plugin.xml @@ -176,6 +176,7 @@ org.jetbrains.plugins.terminal com.jetbrains.restClient JavaScript + com.jetbrains.rider.fsharp + + + +