diff --git a/docs/content/how-tos/rule-configuration.md b/docs/content/how-tos/rule-configuration.md index ceda2c93c..cc5990e68 100644 --- a/docs/content/how-tos/rule-configuration.md +++ b/docs/content/how-tos/rule-configuration.md @@ -128,3 +128,4 @@ The following rules can be specified for linting. - [EnsureTailCallDiagnosticsInRecursiveFunctions (FL0085)](rules/FL0085.html) - [FavourAsKeyword (FL0086)](rules/FL0086.html) - [InterpolatedStringWithNoSubstitution (FL0087)](rules/FL0087.html) +- [NoAsyncRunSynchronouslyInLibrary (FL0088)](rules/FL0088.html) diff --git a/docs/content/how-tos/rules/FL0088.md b/docs/content/how-tos/rules/FL0088.md new file mode 100644 index 000000000..6df26ac1e --- /dev/null +++ b/docs/content/how-tos/rules/FL0088.md @@ -0,0 +1,68 @@ +--- +title: FL0088 +category: how-to +hide_menu: true +--- + +# NoAsyncRunSynchronouslyInLibrary (FL0088) + +*Introduced in `0.26.8`* + +## Cause + +`Async.RunSynchronously` method is used to run async computation in library code. + +The rule assumes the code is in the library if none of the following is true: +- The code is inside NUnit or MSTest test. +- Namespace or project name contains "test" or "console". +- Assembly has `[]` attribute one one of the functions/methods. + +## Rationale + +Using `Async.RunSynchronously` outside of scripts, tests, and console projects can lead to program becoming non-responsive. + +## How To Fix + +Remove `Async.RunSynchronously` and wrap the code that uses `async` computations in `async` computation, using `let!`, `use!`, `match!`, or `return!` keyword to get the result. + +Example: + +```fsharp +type SomeType() = + member self.SomeMethod someParam = + let foo = + asyncSomeFunc someParam + |> Async.RunSynchronously + processFoo foo +``` + +The function can be modified to be asynchronous. In that case it might be better to prefix its name with Async: + +```fsharp +type SomeType() = + member self.AsyncSomeMethod someParam = async { + let! foo = asyncSomeFunc someParam + return processFoo foo + } +``` + +In case the method/function is public, a nice C#-friendly overload that returns `Task<'T>` could be provided, suffixed with Async, that just calls the previous method with `Async.StartAsTask`: + +```fsharp +type SomeType() = + member self.AsyncSomeMethod someParam = async { + let! foo = asyncSomeFunc someParam + return processFoo foo + } + member self.SomeMethodsync someParam = + self.AsyncSomeMethod someParam + |> Async.StartAsTask +``` + +## Rule Settings + + { + "noAsyncRunSynchronouslyInLibrary": { + "enabled": true + } + } diff --git a/src/FSharpLint.Console/Program.fs b/src/FSharpLint.Console/Program.fs index b5e59f28a..a6ebc8202 100644 --- a/src/FSharpLint.Console/Program.fs +++ b/src/FSharpLint.Console/Program.fs @@ -165,9 +165,9 @@ let private start (arguments:ParseResults) (toolsPath:Ionide.ProjInfo. try let lintResult = match fileType with - | FileType.File -> Lint.lintFile lintParams target - | FileType.Source -> Lint.lintSource lintParams target - | FileType.Solution -> Lint.lintSolution lintParams target toolsPath + | FileType.File -> Lint.asyncLintFile lintParams target |> Async.RunSynchronously + | FileType.Source -> Lint.asyncLintSource lintParams target |> Async.RunSynchronously + | FileType.Solution -> Lint.asyncLintSolution lintParams target toolsPath |> Async.RunSynchronously | FileType.Wildcard -> output.WriteInfo "Wildcard detected, but not recommended. Using a project (slnx/sln/fsproj) can detect more issues." let files = expandWildcard target @@ -176,9 +176,9 @@ let private start (arguments:ParseResults) (toolsPath:Ionide.ProjInfo. LintResult.Success List.empty else output.WriteInfo $"Found %d{List.length files} file(s) matching pattern '%s{target}'." - Lint.lintFiles lintParams files + Lint.asyncLintFiles lintParams files |> Async.RunSynchronously | FileType.Project - | _ -> Lint.lintProject lintParams target toolsPath + | _ -> Lint.asyncLintProject lintParams target toolsPath |> Async.RunSynchronously handleLintResult lintResult with | exn -> diff --git a/src/FSharpLint.Core/Application/Configuration.fs b/src/FSharpLint.Core/Application/Configuration.fs index e211e6113..bb07ed237 100644 --- a/src/FSharpLint.Core/Application/Configuration.fs +++ b/src/FSharpLint.Core/Application/Configuration.fs @@ -474,7 +474,8 @@ type Configuration = SuggestUseAutoProperty:EnabledConfig option EnsureTailCallDiagnosticsInRecursiveFunctions:EnabledConfig option FavourAsKeyword:EnabledConfig option - InterpolatedStringWithNoSubstitution:EnabledConfig option } + InterpolatedStringWithNoSubstitution:EnabledConfig option + NoAsyncRunSynchronouslyInLibrary:EnabledConfig option} with static member Zero = { Global = None @@ -570,6 +571,7 @@ with EnsureTailCallDiagnosticsInRecursiveFunctions = None FavourAsKeyword = None InterpolatedStringWithNoSubstitution = None + NoAsyncRunSynchronouslyInLibrary = None } // fsharplint:enable RecordFieldNames @@ -766,6 +768,7 @@ let flattenConfig (config:Configuration) = config.EnsureTailCallDiagnosticsInRecursiveFunctions |> Option.bind (constructRuleIfEnabled EnsureTailCallDiagnosticsInRecursiveFunctions.rule) config.FavourAsKeyword |> Option.bind (constructRuleIfEnabled FavourAsKeyword.rule) config.InterpolatedStringWithNoSubstitution |> Option.bind (constructRuleIfEnabled InterpolatedStringWithNoSubstitution.rule) + config.NoAsyncRunSynchronouslyInLibrary |> Option.bind (constructRuleIfEnabled NoAsyncRunSynchronouslyInLibrary.rule) |] findDeprecation config deprecatedAllRules allRules diff --git a/src/FSharpLint.Core/Application/Lint.fs b/src/FSharpLint.Core/Application/Lint.fs index da988cc96..eca7b1fd3 100644 --- a/src/FSharpLint.Core/Application/Lint.fs +++ b/src/FSharpLint.Core/Application/Lint.fs @@ -124,6 +124,7 @@ module Lint = Rules: RuleMetadata[] GlobalConfig: Rules.GlobalRuleConfig TypeCheckResults: FSharpCheckFileResults option + ProjectCheckResults: FSharpCheckProjectResults option FilePath: string FileContent: string Lines: string[] @@ -147,6 +148,7 @@ module Lint = FileContent = config.FileContent Lines = config.Lines CheckInfo = config.TypeCheckResults + ProjectCheckInfo = config.ProjectCheckResults GlobalConfig = config.GlobalConfig } // Build state for rules with context. @@ -260,6 +262,7 @@ module Lint = Rules = enabledRules.AstNodeRules GlobalConfig = enabledRules.GlobalConfig TypeCheckResults = fileInfo.TypeCheckResults + ProjectCheckResults = fileInfo.ProjectCheckResults FilePath = fileInfo.File FileContent = fileInfo.Text Lines = lines @@ -285,12 +288,6 @@ module Lint = |> Array.iter trySuggest if cancelHasNotBeenRequested () then - let runSynchronously work = - let timeoutMs = 2000 - match lintInfo.CancellationToken with - | Some(cancellationToken) -> Async.RunSynchronously(work, timeoutMs, cancellationToken) - | None -> Async.RunSynchronously(work, timeoutMs) - try let typeChecksSuccessful (typeChecks:(unit -> bool) list) = (true, typeChecks) @@ -420,6 +417,8 @@ module Lint = Source:string /// Optional results of inferring the types on the AST (allows for a more accurate lint). TypeCheckResults:FSharpCheckFileResults option + /// Optional results of project-wide type info (allows for a more accurate lint). + ProjectCheckResults:FSharpCheckProjectResults option } /// Gets a FSharpLint Configuration based on the provided ConfigurationParam. @@ -443,7 +442,7 @@ module Lint = /// Lints an entire F# project by retrieving the files from a given /// path to the `.fsproj` file. - let lintProject (optionalParams:OptionalLintParameters) (projectFilePath:string) (toolsPath:Ionide.ProjInfo.Types.ToolsPath) = + let asyncLintProject (optionalParams:OptionalLintParameters) (projectFilePath:string) (toolsPath:Ionide.ProjInfo.Types.ToolsPath) = async { if IO.File.Exists projectFilePath then let projectFilePath = Path.GetFullPath projectFilePath let lintWarnings = LinkedList() @@ -459,7 +458,7 @@ module Lint = let checker = FSharpChecker.Create(keepAssemblyContents=true) - let parseFilesInProject files projectOptions = + let parseFilesInProject files projectOptions = async { let lintInformation = { Configuration = config CancellationToken = optionalParams.CancellationToken @@ -473,39 +472,50 @@ module Lint = Configuration.IgnoreFiles.shouldFileBeIgnored parsedIgnoreFiles filePath) |> Option.defaultValue false - let parsedFiles = + let! parsedFiles = files |> List.filter (not << isIgnoredFile) - |> List.map (fun file -> ParseFile.parseFile file checker (Some projectOptions) |> Async.RunSynchronously) + |> List.map (fun file -> ParseFile.parseFile file checker (Some projectOptions)) + |> Async.Sequential + + let failedFiles = Array.choose getFailedFiles parsedFiles - let failedFiles = List.choose getFailedFiles parsedFiles + if Array.isEmpty failedFiles then + let! projectCheckResults = checker.ParseAndCheckProject projectOptions - if List.isEmpty failedFiles then parsedFiles - |> List.choose getParsedFiles - |> List.iter (lint lintInformation) + |> Array.choose getParsedFiles + |> Array.iter (fun fileParseResult -> + lint + lintInformation + { fileParseResult with ProjectCheckResults = Some projectCheckResults }) - Success () + return Success () else - Failure (FailedToParseFilesInProject failedFiles) + return Failure (FailedToParseFilesInProject (Array.toList failedFiles)) + } match getProjectInfo projectFilePath toolsPath with | Ok projectOptions -> - match parseFilesInProject (Array.toList projectOptions.SourceFiles) projectOptions with - | Success _ -> lintWarnings |> Seq.toList |> LintResult.Success - | Failure lintFailure -> LintResult.Failure lintFailure + match! parseFilesInProject (Array.toList projectOptions.SourceFiles) projectOptions with + | Success _ -> return lintWarnings |> Seq.toList |> LintResult.Success + | Failure lintFailure -> return LintResult.Failure lintFailure | Error error -> - MSBuildFailedToLoadProjectFile (projectFilePath, BuildFailure.InvalidProjectFileMessage error) - |> LintResult.Failure + return + MSBuildFailedToLoadProjectFile (projectFilePath, BuildFailure.InvalidProjectFileMessage error) + |> LintResult.Failure | Error err -> - RunTimeConfigError err - |> LintResult.Failure + return RunTimeConfigError err |> LintResult.Failure else - FailedToLoadFile projectFilePath - |> LintResult.Failure + return FailedToLoadFile projectFilePath |> LintResult.Failure + } + + [] + let lintProject optionalParams projectFilePath toolsPath = + asyncLintProject optionalParams projectFilePath toolsPath |> Async.RunSynchronously /// Lints an entire F# solution by linting all projects specified in the `.sln`, `slnx` or `.slnf` file. - let lintSolution (optionalParams:OptionalLintParameters) (solutionFilePath:string) (toolsPath:Ionide.ProjInfo.Types.ToolsPath) = + let asyncLintSolution (optionalParams:OptionalLintParameters) (solutionFilePath:string) (toolsPath:Ionide.ProjInfo.Types.ToolsPath) = async { if IO.File.Exists solutionFilePath then let solutionFilePath = Path.GetFullPath solutionFilePath let solutionFolder = Path.GetDirectoryName solutionFilePath @@ -532,9 +542,13 @@ module Lint = projectPath.Replace("\\", "/")) |> Seq.toArray - let (successes, failures) = + let! lintResults = projectsInSolution - |> Array.map (fun projectFilePath -> lintProject optionalParams projectFilePath toolsPath) + |> Array.map (fun projectFilePath -> asyncLintProject optionalParams projectFilePath toolsPath) + |> Async.Sequential + + let (successes, failures) = + lintResults |> Array.fold (fun (successes, failures) result -> match result with | LintResult.Success warnings -> @@ -544,17 +558,21 @@ module Lint = match failures with | [] -> - LintResult.Success successes + return LintResult.Success successes | firstErr :: _ -> - LintResult.Failure firstErr + return LintResult.Failure firstErr with | ex -> - LintResult.Failure (MSBuildFailedToLoadProjectFile (solutionFilePath, BuildFailure.InvalidProjectFileMessage ex.Message)) + return LintResult.Failure (MSBuildFailedToLoadProjectFile (solutionFilePath, BuildFailure.InvalidProjectFileMessage ex.Message)) - | Error err -> LintResult.Failure (RunTimeConfigError err) + | Error err -> return LintResult.Failure (RunTimeConfigError err) else - FailedToLoadFile solutionFilePath - |> LintResult.Failure + return FailedToLoadFile solutionFilePath |> LintResult.Failure + } + + [] + let lintSolution optionalParams solutionFilePath toolsPath = + asyncLintSolution optionalParams solutionFilePath toolsPath |> Async.RunSynchronously /// Lints F# source code that has already been parsed using `FSharp.Compiler.Services` in the calling application. let lintParsedSource optionalParams parsedFileInfo = @@ -576,6 +594,7 @@ module Lint = { ParseFile.Text = parsedFileInfo.Source ParseFile.Ast = parsedFileInfo.Ast ParseFile.TypeCheckResults = parsedFileInfo.TypeCheckResults + ParseFile.ProjectCheckResults = parsedFileInfo.ProjectCheckResults ParseFile.File = "" } lint lintInformation parsedFileInfo @@ -594,13 +613,14 @@ module Lint = let parsedFileInfo = { Source = parseFileInformation.Text Ast = parseFileInformation.Ast - TypeCheckResults = parseFileInformation.TypeCheckResults } + TypeCheckResults = parseFileInformation.TypeCheckResults + ProjectCheckResults = None } return lintParsedSource optionalParams parsedFileInfo | ParseFile.Failed failure -> return LintResult.Failure(FailedToParseFile failure) } - /// Lints F# source code. + [] let lintSource optionalParams source = asyncLintSource optionalParams source |> Async.RunSynchronously @@ -625,6 +645,7 @@ module Lint = { ParseFile.Text = parsedFileInfo.Source ParseFile.Ast = parsedFileInfo.Ast ParseFile.TypeCheckResults = parsedFileInfo.TypeCheckResults + ParseFile.ProjectCheckResults = parsedFileInfo.ProjectCheckResults ParseFile.File = filePath } lint lintInformation parsedFileInfo @@ -633,52 +654,70 @@ module Lint = | Error err -> LintResult.Failure (RunTimeConfigError err) /// Lints an F# file from a given path to the `.fs` file. - let lintFile optionalParams filePath = + let asyncLintFile optionalParams filePath = async { if IO.File.Exists filePath then let checker = FSharpChecker.Create(keepAssemblyContents=true) - match ParseFile.parseFile filePath checker None |> Async.RunSynchronously with + match! ParseFile.parseFile filePath checker None with | ParseFile.Success astFileParseInfo -> let parsedFileInfo = { Source = astFileParseInfo.Text Ast = astFileParseInfo.Ast - TypeCheckResults = astFileParseInfo.TypeCheckResults } + TypeCheckResults = astFileParseInfo.TypeCheckResults + ProjectCheckResults = astFileParseInfo.ProjectCheckResults } - lintParsedFile optionalParams parsedFileInfo filePath - | ParseFile.Failed failure -> LintResult.Failure(FailedToParseFile failure) + return lintParsedFile optionalParams parsedFileInfo filePath + | ParseFile.Failed failure -> return LintResult.Failure(FailedToParseFile failure) else - FailedToLoadFile filePath - |> LintResult.Failure + return FailedToLoadFile filePath |> LintResult.Failure + } + + [] + let lintFile optionalParams filePath = + asyncLintFile optionalParams filePath |> Async.RunSynchronously /// Lints multiple F# files from given file paths. - let lintFiles optionalParams filePaths = + let asyncLintFiles optionalParams filePaths = async { let checker = FSharpChecker.Create(keepAssemblyContents=true) match getConfig optionalParams.Configuration with | Ok config -> let optionalParams = { optionalParams with Configuration = ConfigurationParam.Configuration config } - let lintSingleFile filePath = + let lintSingleFile filePath = async { if IO.File.Exists filePath then - match ParseFile.parseFile filePath checker None |> Async.RunSynchronously with + match! ParseFile.parseFile filePath checker None with | ParseFile.Success astFileParseInfo -> let parsedFileInfo = { Source = astFileParseInfo.Text Ast = astFileParseInfo.Ast - TypeCheckResults = astFileParseInfo.TypeCheckResults } - lintParsedFile optionalParams parsedFileInfo filePath + TypeCheckResults = astFileParseInfo.TypeCheckResults + ProjectCheckResults = astFileParseInfo.ProjectCheckResults } + return lintParsedFile optionalParams parsedFileInfo filePath | ParseFile.Failed failure -> - LintResult.Failure (FailedToParseFile failure) + return LintResult.Failure (FailedToParseFile failure) else - LintResult.Failure (FailedToLoadFile filePath) + return LintResult.Failure (FailedToLoadFile filePath) + } - let results = filePaths |> Seq.map lintSingleFile |> Seq.toList + let! results = filePaths |> Seq.map lintSingleFile |> Async.Sequential - let failures = results |> List.choose (function | LintResult.Failure failure -> Some failure | _ -> None) - let warnings = results |> List.collect (function | LintResult.Success warning -> warning | _ -> List.empty) + let failures = + results + |> Seq.choose (function | LintResult.Failure failure -> Some failure | _ -> None) + |> Seq.toList + let warnings = + results + |> Seq.collect (function | LintResult.Success warning -> warning | _ -> List.empty) + |> Seq.toList match failures with - | firstFailure :: _ -> LintResult.Failure firstFailure - | [] -> LintResult.Success warnings + | firstFailure :: _ -> return LintResult.Failure firstFailure + | [] -> return LintResult.Success warnings | Error err -> - LintResult.Failure (RunTimeConfigError err) + return LintResult.Failure (RunTimeConfigError err) + } + + [] + let lintFiles optionalParams filePaths = + asyncLintFiles optionalParams filePaths |> Async.RunSynchronously diff --git a/src/FSharpLint.Core/Application/Lint.fsi b/src/FSharpLint.Core/Application/Lint.fsi index 29ddd3332..618d48317 100644 --- a/src/FSharpLint.Core/Application/Lint.fsi +++ b/src/FSharpLint.Core/Application/Lint.fsi @@ -74,6 +74,9 @@ module Lint = /// Optional results of inferring the types on the AST (allows for a more accurate lint). TypeCheckResults: FSharpCheckFileResults option + + /// Optional results of project-wide type info (allows for a more accurate lint). + ProjectCheckResults:FSharpCheckProjectResults option } type BuildFailure = | InvalidProjectFileMessage of string @@ -124,6 +127,7 @@ module Lint = Rules: RuleMetadata[] GlobalConfig: Rules.GlobalRuleConfig TypeCheckResults: FSharpCheckFileResults option + ProjectCheckResults: FSharpCheckProjectResults option FilePath: string FileContent: string Lines: string[] @@ -147,26 +151,38 @@ module Lint = val runLineRules : RunLineRulesConfig -> Suggestion.LintWarning [] /// Lints an entire F# solution by linting all projects specified in the `.sln`, `slnx` or `.slnf` file. + val asyncLintSolution : optionalParams:OptionalLintParameters -> solutionFilePath:string -> toolsPath:Ionide.ProjInfo.Types.ToolsPath -> Async + + /// [Obsolete] Lints an entire F# solution by linting all projects specified in the `.sln`, `slnx` or `.slnf` file. val lintSolution : optionalParams:OptionalLintParameters -> solutionFilePath:string -> toolsPath:Ionide.ProjInfo.Types.ToolsPath -> LintResult /// Lints an entire F# project by retrieving the files from a given /// path to the `.fsproj` file. - val lintProject : optionalParams:OptionalLintParameters -> projectFilePath:string -> toolsPath:Ionide.ProjInfo.Types.ToolsPath -> LintResult + val asyncLintProject : optionalParams:OptionalLintParameters -> projectFilePath:string -> toolsPath:Ionide.ProjInfo.Types.ToolsPath -> Async - /// Lints F# source code. - val lintSource : optionalParams:OptionalLintParameters -> source:string -> LintResult + /// [Obsolete] Lints an entire F# project by retrieving the files from a given path to the `.fsproj` file. + val lintProject : optionalParams:OptionalLintParameters -> projectFilePath:string -> toolsPath:Ionide.ProjInfo.Types.ToolsPath -> LintResult /// Lints F# source code async. val asyncLintSource : optionalParams:OptionalLintParameters -> source:string -> Async + /// [Obsolete] Lints F# source code. + val lintSource : optionalParams:OptionalLintParameters -> source:string -> LintResult + /// Lints F# source code that has already been parsed using /// `FSharp.Compiler.Services` in the calling application. val lintParsedSource : optionalParams:OptionalLintParameters -> parsedFileInfo:ParsedFileInformation -> LintResult /// Lints an F# file from a given path to the `.fs` file. + val asyncLintFile : optionalParams:OptionalLintParameters -> filePath:string -> Async + + /// [Obsolete] Lints an F# file from a given path to the `.fs` file. val lintFile : optionalParams:OptionalLintParameters -> filePath:string -> LintResult /// Lints multiple F# files from given file paths. + val asyncLintFiles : optionalParams:OptionalLintParameters -> filePaths:string seq -> Async + + /// [Obsolete] Lints multiple F# files from given file paths. val lintFiles : optionalParams:OptionalLintParameters -> filePaths:string seq -> LintResult /// Lints an F# file that has already been parsed using diff --git a/src/FSharpLint.Core/FSharpLint.Core.fsproj b/src/FSharpLint.Core/FSharpLint.Core.fsproj index 6f0f5d10e..456ed59ee 100644 --- a/src/FSharpLint.Core/FSharpLint.Core.fsproj +++ b/src/FSharpLint.Core/FSharpLint.Core.fsproj @@ -69,6 +69,7 @@ + diff --git a/src/FSharpLint.Core/Framework/ParseFile.fs b/src/FSharpLint.Core/Framework/ParseFile.fs index 6a5edcb56..012622881 100644 --- a/src/FSharpLint.Core/Framework/ParseFile.fs +++ b/src/FSharpLint.Core/Framework/ParseFile.fs @@ -23,6 +23,9 @@ module ParseFile = /// Optional results of inferring the types on the AST (allows for a more accurate lint). TypeCheckResults:FSharpCheckFileResults option + /// Optional results of project-wide type info (allows for a more accurate lint). + ProjectCheckResults:FSharpCheckProjectResults option + /// Path to the file. File:string } @@ -49,6 +52,7 @@ module ParseFile = Text = source Ast = parseResults.ParseTree TypeCheckResults = Some(typeCheckResults) + ProjectCheckResults = None File = file } | FSharpCheckFileAnswer.Aborted -> return Failed(AbortedTypeCheck) diff --git a/src/FSharpLint.Core/Framework/Rules.fs b/src/FSharpLint.Core/Framework/Rules.fs index 2877244ab..29057abe8 100644 --- a/src/FSharpLint.Core/Framework/Rules.fs +++ b/src/FSharpLint.Core/Framework/Rules.fs @@ -30,6 +30,7 @@ type AstNodeRuleParams = FileContent:string Lines:string [] CheckInfo:FSharpCheckFileResults option + ProjectCheckInfo:FSharpCheckProjectResults option GlobalConfig:GlobalRuleConfig } type LineRuleParams = diff --git a/src/FSharpLint.Core/Rules/Conventions/NoAsyncRunSynchronouslyInLibrary.fs b/src/FSharpLint.Core/Rules/Conventions/NoAsyncRunSynchronouslyInLibrary.fs new file mode 100644 index 000000000..7c64fc582 --- /dev/null +++ b/src/FSharpLint.Core/Rules/Conventions/NoAsyncRunSynchronouslyInLibrary.fs @@ -0,0 +1,134 @@ +module FSharpLint.Rules.NoAsyncRunSynchronouslyInLibrary + +open FSharp.Compiler.Syntax +open FSharp.Compiler.Symbols +open FSharp.Compiler.Text +open FSharp.Compiler.CodeAnalysis +open FSharpLint.Framework +open FSharpLint.Framework.Suggestion +open FSharpLint.Framework.Ast +open FSharpLint.Framework.Rules +open FSharpLint.Framework.Utilities + +let hasEntryPoint (checkFileResults: FSharpCheckFileResults) (maybeProjectCheckResults: FSharpCheckProjectResults option) = + let hasEntryPointInTheSameFile = + match checkFileResults.ImplementationFile with + | Some implFile -> implFile.HasExplicitEntryPoint + | None -> false + + hasEntryPointInTheSameFile + || + match maybeProjectCheckResults with + | Some projectCheckResults -> + projectCheckResults.AssemblyContents.ImplementationFiles + |> Seq.exists (fun implFile -> implFile.HasExplicitEntryPoint) + | None -> false + +let excludedProjectNames = [ "test"; "console" ] + +let isInTestProject (checkFileResults: FSharpCheckFileResults) = + let namespaceIncludesTest = + match checkFileResults.ImplementationFile with + | Some implFile -> + excludedProjectNames |> List.exists (fun name -> implFile.QualifiedName.ToLowerInvariant().Contains name) + | None -> false + let projectFileInfo = System.IO.FileInfo checkFileResults.ProjectContext.ProjectOptions.ProjectFileName + namespaceIncludesTest + || excludedProjectNames |> List.exists (fun name -> projectFileInfo.Name.ToLowerInvariant().Contains name) + +let extractAttributeNames (attributes: SynAttributes) = + seq { + for attr in extractAttributes attributes do + match attr.TypeName with + | SynLongIdent([ident], _, _) -> yield ident.idText + | _ -> () + } + +let testMethodAttributes = [ "Test"; "TestMethod" ] +let testClassAttributes = [ "TestFixture"; "TestClass" ] + +let isInTheSameModuleAsTest (nodes: array) (maybeProjectCheckResults: FSharpCheckProjectResults option) = + let isTestMethodOrClass node = + match node with + | AstNode.MemberDefinition(SynMemberDefn.Member(SynBinding(_, _, _, _, attributes, _, _, _, _, _, _, _, _), _)) -> + attributes + |> extractAttributeNames + |> Seq.exists (fun name -> testMethodAttributes |> List.contains name) + | AstNode.TypeDefinition(SynTypeDefn.SynTypeDefn(SynComponentInfo(attributes, _, _, _, _, _, _, _), _, _, _, _, _)) -> + attributes + |> extractAttributeNames + |> Seq.exists (fun name -> testClassAttributes |> List.contains name) + | _ -> false + + let isDeclarationOfTestClass declaration = + match declaration with + | FSharpImplementationFileDeclaration.Entity(entity, _) -> + entity.Attributes + |> Seq.exists (fun attr -> testClassAttributes |> List.contains attr.AttributeType.DisplayName) + | _ -> false + + match maybeProjectCheckResults with + | Some projectCheckResults -> + projectCheckResults.AssemblyContents.ImplementationFiles + |> Seq.exists (fun implFile -> + implFile.Declarations + |> Seq.exists isDeclarationOfTestClass + ) + | None -> + nodes |> Array.exists (fun node -> isTestMethodOrClass node.Actual) + +let isInObsoleteMethodOrFunction parents = + let isObsolete node = + match node with + | AstNode.MemberDefinition(SynMemberDefn.Member(SynBinding(_, _, _, _, attributes, _, _, _, _, _, _, _, _), _)) -> + attributes + |> extractAttributeNames + |> Seq.contains "Obsolete" + | AstNode.Binding(SynBinding(_, _, _, _, attributes, _, _, _, _, _, _, _, _)) -> + attributes + |> extractAttributeNames + |> Seq.contains "Obsolete" + | _ -> false + + parents |> List.exists isObsolete + +let checkIfInLibrary (args: AstNodeRuleParams) (range: range) : array = + let ruleNotApplicable = + match args.CheckInfo with + | Some checkFileResults -> + hasEntryPoint checkFileResults args.ProjectCheckInfo + || isInTestProject checkFileResults + || isInObsoleteMethodOrFunction (args.GetParents args.NodeIndex) + || isInTheSameModuleAsTest args.SyntaxArray args.ProjectCheckInfo + | None -> + isInObsoleteMethodOrFunction (args.GetParents args.NodeIndex) + || isInTheSameModuleAsTest args.SyntaxArray args.ProjectCheckInfo + + if ruleNotApplicable then + Array.empty + else + Array.singleton + { + Range = range + Message = Resources.GetString "NoAsyncRunSynchronouslyInLibrary" + SuggestedFix = None + TypeChecks = List.Empty + } + +let runner args = + match args.AstNode with + | AstNode.Identifier(["Async"; "RunSynchronously"], range) -> + checkIfInLibrary args range + | _ -> Array.empty + +let rule = + AstNodeRule + { + Name = "NoAsyncRunSynchronouslyInLibrary" + Identifier = Identifiers.NoAsyncRunSynchronouslyInLibrary + RuleConfig = + { + AstNodeRuleConfig.Runner = runner + Cleanup = ignore + } + } diff --git a/src/FSharpLint.Core/Rules/Identifiers.fs b/src/FSharpLint.Core/Rules/Identifiers.fs index e40b6edb6..3106e12c3 100644 --- a/src/FSharpLint.Core/Rules/Identifiers.fs +++ b/src/FSharpLint.Core/Rules/Identifiers.fs @@ -92,3 +92,4 @@ let FavourNonMutablePropertyInitialization = identifier 84 let EnsureTailCallDiagnosticsInRecursiveFunctions = identifier 85 let FavourAsKeyword = identifier 86 let InterpolatedStringWithNoSubstitution = identifier 87 +let NoAsyncRunSynchronouslyInLibrary = identifier 88 diff --git a/src/FSharpLint.Core/Text.resx b/src/FSharpLint.Core/Text.resx index e986c1672..24f581992 100644 --- a/src/FSharpLint.Core/Text.resx +++ b/src/FSharpLint.Core/Text.resx @@ -384,4 +384,7 @@ Do not use interpolated string syntax (with $ prefix) or formatting functions (sprintf, failwithf) when not really performing any interpolation. + + Async.RunSynchronously should not be used in libraries. + diff --git a/src/FSharpLint.Core/fsharplint.json b/src/FSharpLint.Core/fsharplint.json index f97777e45..36e1fad4a 100644 --- a/src/FSharpLint.Core/fsharplint.json +++ b/src/FSharpLint.Core/fsharplint.json @@ -334,6 +334,7 @@ "ensureTailCallDiagnosticsInRecursiveFunctions": { "enabled": true }, "favourAsKeyword": { "enabled": true }, "interpolatedStringWithNoSubstitution": { "enabled": true }, + "noAsyncRunSynchronouslyInLibrary": { "enabled": true }, "hints": { "add": [ "not (a = b) ===> a <> b", diff --git a/tests/FSharpLint.Benchmarks/Benchmark.fs b/tests/FSharpLint.Benchmarks/Benchmark.fs index 63f5e703c..4e8b536ef 100644 --- a/tests/FSharpLint.Benchmarks/Benchmark.fs +++ b/tests/FSharpLint.Benchmarks/Benchmark.fs @@ -30,7 +30,7 @@ type Benchmark () = let (fileInfo, lines) = let text = File.ReadAllText sourceFile let tree = generateAst text sourceFile - ({ Ast = tree; Source = text; TypeCheckResults = None }, String.toLines text |> Array.toList) + ({ Ast = tree; Source = text; TypeCheckResults = None; ProjectCheckResults = None }, String.toLines text |> Array.toList) [] member this.LintParsedFile () = diff --git a/tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj b/tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj index 373cef93c..0923126f1 100644 --- a/tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj +++ b/tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj @@ -47,6 +47,7 @@ + diff --git a/tests/FSharpLint.Core.Tests/Rules/Conventions/NoAsyncRunSynchronouslyInLibrary.fs b/tests/FSharpLint.Core.Tests/Rules/Conventions/NoAsyncRunSynchronouslyInLibrary.fs new file mode 100644 index 000000000..03e6f67c2 --- /dev/null +++ b/tests/FSharpLint.Core.Tests/Rules/Conventions/NoAsyncRunSynchronouslyInLibrary.fs @@ -0,0 +1,132 @@ +module FSharpLint.Core.Tests.Rules.Conventions.NoAsyncRunSynchronouslyInLibrary + +open NUnit.Framework +open FSharpLint.Framework.Rules +open FSharpLint.Rules + +[] +type TestNoAsyncRunSynchronouslyInLibrary() = + inherit FSharpLint.Core.Tests.TestAstNodeRuleBase.TestAstNodeRuleBase(NoAsyncRunSynchronouslyInLibrary.rule) + + [] + member this.``Async.RunSynchronously should not be used in library code``() = + this.Parse(""" +module Program + +async { + return () +} +|> Async.RunSynchronously""") + + Assert.IsTrue this.ErrorsExist + + [] + member this.``Async.RunSynchronously may be used in code that declares entry point``() = + this.Parse(""" +module Program + +[] +let main () = + async { + return () + } + |> Async.RunSynchronously""") + + this.AssertNoWarnings() + + [] + member this.``Async.RunSynchronously may be used in code module that has function with entry point``() = + this.Parse(""" +module Program + +let foo () = + async { + return () + } + |> Async.RunSynchronously + +[] +let main () = + 0""") + + this.AssertNoWarnings() + + [] + member this.``Async.RunSynchronously may be used in NUnit test code``() = + this.Parse(""" +module Program + +[] +type FooTest () = + [] + member this.Foo() = + async { + return () + } + |> Async.RunSynchronously""") + + this.AssertNoWarnings() + + [] + member this.``Async.RunSynchronously may be used in MSTest test code``() = + this.Parse(""" +module Program + +[] +type FooTest () = + [] + member this.Foo() = + async { + return () + } + |> Async.RunSynchronously""") + + this.AssertNoWarnings() + + [] + member this.``Async.RunSynchronously may be used in module with tests``() = + this.Parse(""" +module Program + +let foo () = + async { + return () + } + |> Async.RunSynchronously + +[] +type FooTest () = + [] + member this.Foo() = + ()""") + + this.AssertNoWarnings() + + [] + member this.``Async.RunSynchronously may be used in methods with Obsolete attribute``() = + this.Parse(""" +module Program + +type FooTest () = + [] + member this.Foo() = + async { + return () + } + |> Async.RunSynchronously""") + + this.AssertNoWarnings() + + [] + member this.``Async.RunSynchronously may be used in functions with Obsolete attribute``() = + this.Parse(""" +module Program + +[] +let Foo() = + async { + return () + } + |> Async.RunSynchronously""") + + this.AssertNoWarnings() diff --git a/tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs b/tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs index 0f78b6f19..425a3e5bd 100644 --- a/tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs +++ b/tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs @@ -42,6 +42,7 @@ type TestAstNodeRuleBase (rule:Rule) = Rules = Array.singleton rule GlobalConfig = globalConfig TypeCheckResults = checkResult + ProjectCheckResults = None FilePath = (Option.defaultValue String.Empty fileName) FileContent = input Lines = (input.Split("\n")) diff --git a/tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs b/tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs index 5f2222309..6fe9dbe11 100644 --- a/tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs +++ b/tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs @@ -64,6 +64,7 @@ type TestHintMatcherBase () = Rules = Array.singleton rule GlobalConfig = globalConfig TypeCheckResults = checkResult + ProjectCheckResults = None FilePath = (Option.defaultValue String.Empty fileName) FileContent = input Lines = (input.Split("\n")) diff --git a/tests/FSharpLint.Core.Tests/Rules/TestIndentationRule.fs b/tests/FSharpLint.Core.Tests/Rules/TestIndentationRule.fs index 8abd41702..e2d2fd68b 100644 --- a/tests/FSharpLint.Core.Tests/Rules/TestIndentationRule.fs +++ b/tests/FSharpLint.Core.Tests/Rules/TestIndentationRule.fs @@ -37,6 +37,7 @@ type TestIndentationRuleBase (rule:Rule) = Rules = Array.empty GlobalConfig = globalConfig TypeCheckResults = None + ProjectCheckResults = None FilePath = fileName FileContent = input Lines = lines diff --git a/tests/FSharpLint.Core.Tests/Rules/TestLineRule.fs b/tests/FSharpLint.Core.Tests/Rules/TestLineRule.fs index c9c46cf02..480e55f82 100644 --- a/tests/FSharpLint.Core.Tests/Rules/TestLineRule.fs +++ b/tests/FSharpLint.Core.Tests/Rules/TestLineRule.fs @@ -37,6 +37,7 @@ type TestLineRuleBase (rule:Rule) = Rules = Array.empty GlobalConfig = globalConfig TypeCheckResults = None + ProjectCheckResults = None FilePath = fileName FileContent = input Lines = lines diff --git a/tests/FSharpLint.Core.Tests/Rules/TestNoTabCharactersRule.fs b/tests/FSharpLint.Core.Tests/Rules/TestNoTabCharactersRule.fs index 2807d7ad8..ccfcaccf2 100644 --- a/tests/FSharpLint.Core.Tests/Rules/TestNoTabCharactersRule.fs +++ b/tests/FSharpLint.Core.Tests/Rules/TestNoTabCharactersRule.fs @@ -37,6 +37,7 @@ type TestNoTabCharactersRuleBase (rule:Rule) = Rules = Array.empty GlobalConfig = globalConfig TypeCheckResults = None + ProjectCheckResults = None FilePath = fileName FileContent = input Lines = lines diff --git a/tests/FSharpLint.FunctionalTest/TestApi.fs b/tests/FSharpLint.FunctionalTest/TestApi.fs index d7fe3282b..635113f10 100644 --- a/tests/FSharpLint.FunctionalTest/TestApi.fs +++ b/tests/FSharpLint.FunctionalTest/TestApi.fs @@ -38,7 +38,7 @@ module TestApi = member _.``Performance of linting an existing file``() = let text = File.ReadAllText sourceFile let tree = generateAst text - let fileInfo = { Ast = tree; Source = text; TypeCheckResults = None } + let fileInfo = { Ast = tree; Source = text; TypeCheckResults = None; ProjectCheckResults = None } let stopwatch = Stopwatch.StartNew() let times = ResizeArray() @@ -64,7 +64,7 @@ module TestApi = let projectPath = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" let projectFile = projectPath "FSharpLint.FunctionalTest.TestedProject.NetCore.fsproj" - let result = lintProject OptionalLintParameters.Default projectFile toolsPath + let result = asyncLintProject OptionalLintParameters.Default projectFile toolsPath |> Async.RunSynchronously match result with | LintResult.Success warnings -> @@ -77,7 +77,7 @@ module TestApi = let projectPath = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore" let projectFile = projectPath "FSharpLint.FunctionalTest.TestedProject.NetCore.fsproj" - let result = lintProject OptionalLintParameters.Default projectFile toolsPath + let result = asyncLintProject OptionalLintParameters.Default projectFile toolsPath |> Async.RunSynchronously match result with | LintResult.Success warnings -> @@ -92,7 +92,7 @@ module TestApi = let tempConfigFile = TestContext.CurrentContext.TestDirectory "fsharplint.json" File.WriteAllText (tempConfigFile, """{ "ignoreFiles": ["*"] }""") - let result = lintProject OptionalLintParameters.Default projectFile toolsPath + let result = asyncLintProject OptionalLintParameters.Default projectFile toolsPath |> Async.RunSynchronously File.Delete tempConfigFile match result with @@ -108,7 +108,7 @@ module TestApi = let projectPath = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" let solutionFile = projectPath solutionFileName - let result = lintSolution OptionalLintParameters.Default solutionFile toolsPath + let result = asyncLintSolution OptionalLintParameters.Default solutionFile toolsPath |> Async.RunSynchronously match result with | LintResult.Success warnings -> @@ -126,7 +126,9 @@ module TestApi = let relativePathToSolutionFile = Path.GetRelativePath (Directory.GetCurrentDirectory(), solutionFile) - let result = lintSolution OptionalLintParameters.Default relativePathToSolutionFile toolsPath + let result = + asyncLintSolution OptionalLintParameters.Default relativePathToSolutionFile toolsPath + |> Async.RunSynchronously match result with | LintResult.Success warnings ->