diff --git a/NpgsqlFSharpAnalyzer.sln b/NpgsqlFSharpAnalyzer.sln index 4701772..844c30c 100644 --- a/NpgsqlFSharpAnalyzer.sln +++ b/NpgsqlFSharpAnalyzer.sln @@ -41,6 +41,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FParsec", "src\FParsec\FPar EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParserTestsWithNet48", "src\ParserTestsWithNet48\ParserTestsWithNet48.csproj", "{40067E09-6281-4DC2-905D-60F7C6E3B812}" EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Ubik", "src\Ubik\Ubik.fsproj", "{0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -159,6 +161,18 @@ Global {40067E09-6281-4DC2-905D-60F7C6E3B812}.Release|x64.Build.0 = Release|Any CPU {40067E09-6281-4DC2-905D-60F7C6E3B812}.Release|x86.ActiveCfg = Release|Any CPU {40067E09-6281-4DC2-905D-60F7C6E3B812}.Release|x86.Build.0 = Release|Any CPU + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}.Debug|x64.ActiveCfg = Debug|Any CPU + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}.Debug|x64.Build.0 = Debug|Any CPU + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}.Debug|x86.ActiveCfg = Debug|Any CPU + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}.Debug|x86.Build.0 = Debug|Any CPU + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}.Release|Any CPU.Build.0 = Release|Any CPU + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}.Release|x64.ActiveCfg = Release|Any CPU + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}.Release|x64.Build.0 = Release|Any CPU + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}.Release|x86.ActiveCfg = Release|Any CPU + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -174,6 +188,7 @@ Global {C5EB813F-4278-4EE7-925B-6757BAD0FE9B} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216} {9C8E7641-9DC8-470C-8009-71A747C01DC5} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216} {40067E09-6281-4DC2-905D-60F7C6E3B812} = {ACBEE43C-7A88-4FB1-9B06-DB064D22B29F} + {0CCC7E3F-78F0-4A4F-8AC1-27A1F6386B1E} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BC821061-2FB3-4ABD-9FA1-044D4C59C475} diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 46a7474..f6538ba 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,6 @@ +### 3.18.0 - 2020-09-15 +* Analyze SQL blocks from within lambda expressions + ### 3.17.0 - 2020-09-15 * Support for datetimeOffset and datetimeOffsetOrNone when reading columns of type timestamptz diff --git a/src/FParsec/AssemblyInfo.fs b/src/FParsec/AssemblyInfo.fs index a687815..bc891f6 100644 --- a/src/FParsec/AssemblyInfo.fs +++ b/src/FParsec/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] +[] [] -[] -[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FParsec" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.17.0" + let [] AssemblyVersion = "3.18.0" let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.17.0" - let [] AssemblyInformationalVersion = "3.17.0" + let [] AssemblyFileVersion = "3.18.0" + let [] AssemblyInformationalVersion = "3.18.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "985f38c696391f5c8f38f4498e3a59db66ffe3c6" + let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" diff --git a/src/NpgsqlFSharpAnalyzer.Core/AssemblyInfo.fs b/src/NpgsqlFSharpAnalyzer.Core/AssemblyInfo.fs index c7f7628..20437bd 100644 --- a/src/NpgsqlFSharpAnalyzer.Core/AssemblyInfo.fs +++ b/src/NpgsqlFSharpAnalyzer.Core/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] +[] [] -[] -[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "NpgsqlFSharpAnalyzer.Core" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.17.0" + let [] AssemblyVersion = "3.18.0" let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.17.0" - let [] AssemblyInformationalVersion = "3.17.0" + let [] AssemblyFileVersion = "3.18.0" + let [] AssemblyInformationalVersion = "3.18.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "985f38c696391f5c8f38f4498e3a59db66ffe3c6" + let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" diff --git a/src/NpgsqlFSharpAnalyzer.Core/SyntacticAnalysis.fs b/src/NpgsqlFSharpAnalyzer.Core/SyntacticAnalysis.fs index 3d20dde..5305c34 100644 --- a/src/NpgsqlFSharpAnalyzer.Core/SyntacticAnalysis.fs +++ b/src/NpgsqlFSharpAnalyzer.Core/SyntacticAnalysis.fs @@ -641,6 +641,9 @@ module SyntacticAnalysis = | Some expr -> yield! visitSyntacticExpression expr range ] + | SynExpr.Lambda (fromMethod, inSeq, args, body, range) -> + visitSyntacticExpression body range + | otherwise -> [ ] diff --git a/src/NpgsqlFSharpAnalyzer/AssemblyInfo.fs b/src/NpgsqlFSharpAnalyzer/AssemblyInfo.fs index 70460cb..05f6c84 100644 --- a/src/NpgsqlFSharpAnalyzer/AssemblyInfo.fs +++ b/src/NpgsqlFSharpAnalyzer/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] +[] [] -[] -[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "NpgsqlFSharpAnalyzer" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.17.0" + let [] AssemblyVersion = "3.18.0" let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.17.0" - let [] AssemblyInformationalVersion = "3.17.0" + let [] AssemblyFileVersion = "3.18.0" + let [] AssemblyInformationalVersion = "3.18.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "985f38c696391f5c8f38f4498e3a59db66ffe3c6" + let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" diff --git a/src/NpgsqlFSharpParser/AssemblyInfo.fs b/src/NpgsqlFSharpParser/AssemblyInfo.fs index 97f42cf..d717dbe 100644 --- a/src/NpgsqlFSharpParser/AssemblyInfo.fs +++ b/src/NpgsqlFSharpParser/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] +[] [] -[] -[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "NpgsqlFSharpParser" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.17.0" + let [] AssemblyVersion = "3.18.0" let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.17.0" - let [] AssemblyInformationalVersion = "3.17.0" + let [] AssemblyFileVersion = "3.18.0" + let [] AssemblyInformationalVersion = "3.18.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "985f38c696391f5c8f38f4498e3a59db66ffe3c6" + let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" diff --git a/src/NpgsqlFSharpVs/source.extension.vsixmanifest b/src/NpgsqlFSharpVs/source.extension.vsixmanifest index 03e2b70..da6b243 100644 --- a/src/NpgsqlFSharpVs/source.extension.vsixmanifest +++ b/src/NpgsqlFSharpVs/source.extension.vsixmanifest @@ -3,7 +3,7 @@ xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011"> - + NpgsqlFSharpVs F# Analyzer for embedded SQL syntax analysis, type-checking for parameters and result sets and nullable column detection when writing queries using Npgsql.FSharp. https://github.com/Zaid-Ajaj/Npgsql.FSharp.Analyzer diff --git a/src/Ubik/AssemblyInfo.fs b/src/Ubik/AssemblyInfo.fs new file mode 100644 index 0000000..0efe71c --- /dev/null +++ b/src/Ubik/AssemblyInfo.fs @@ -0,0 +1,23 @@ +// Auto-Generated by FAKE; do not edit +namespace System +open System.Reflection + +[] +[] +[] +[] +[] +[] +[] +[] +do () + +module internal AssemblyVersionInformation = + let [] AssemblyTitle = "Ubik" + let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" + let [] AssemblyVersion = "3.18.0" + let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" + let [] AssemblyFileVersion = "3.18.0" + let [] AssemblyInformationalVersion = "3.18.0" + let [] AssemblyMetadata_ReleaseChannel = "release" + let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" diff --git a/src/Ubik/Program.fs b/src/Ubik/Program.fs new file mode 100644 index 0000000..89cc6aa --- /dev/null +++ b/src/Ubik/Program.fs @@ -0,0 +1,79 @@ +open System +open System.IO +open Npgsql.FSharp.Analyzers.Core +open Spectre.Console +open System.Xml + +let resolveFile (path: string) = + if Path.IsPathRooted path + then path + else Path.GetFullPath (Path.Combine(Environment.CurrentDirectory, path)) + +let getProject (args: string []) = + try + match args with + | [| |] -> + Directory.GetFiles(Environment.CurrentDirectory, "*.fsproj") + |> Array.tryHead + |> Option.map (fun projectPath -> resolveFile projectPath) + + | multipleArgs -> + let firstArg = multipleArgs.[0] + if firstArg.EndsWith(".fsproj") then + Some (resolveFile firstArg) + else + Directory.GetFiles(resolveFile firstArg, "*.fsproj") + |> Array.tryHead + |> Option.map (fun projectPath -> resolveFile projectPath) + with + | error -> None + +[] +let main argv = + match getProject argv with + | None -> + printfn "No project file found in the current directory" + 1 + + | Some project -> + AnsiConsole.MarkupLine("Analyzing [blue]{0}[/]", project) + + let document = XmlDocument() + document.LoadXml(File.ReadAllText project) + + let fsharpFileNodes = document.GetElementsByTagName("Compile") + let fsharpFiles = [ + for item in 0 .. fsharpFileNodes.Count - 1 -> + let relativePath = fsharpFileNodes.[item].Attributes.["Include"].InnerText + let projectParent = Directory.GetParent project + Path.Combine(projectParent.FullName, relativePath) + ] + + for file in fsharpFiles do + AnsiConsole.MarkupLine("Analyzing file [green]{0}[/]", file) + match Project.context file with + | None -> () + | Some context -> + let syntacticBlocks = SyntacticAnalysis.findSqlOperations context + if not syntacticBlocks.IsEmpty then + let messages = + let connectionString = SqlAnalyzer.tryFindConnectionString context.FileName + if isNull connectionString || String.IsNullOrWhiteSpace connectionString then + [ ] + else + match SqlAnalysis.databaseSchema connectionString with + | Result.Error connectionError -> + [ + for block in syntacticBlocks -> + SqlAnalysis.createWarning (sprintf "Error while connecting to the development database using the connection string from environment variable 'NPGSQL_FSHARP' or put the connection string in a file called 'NPGSQL_FSHARP' relative next your project or in your project root. Connection error: %s" connectionError) block.range + ] + + | Result.Ok schema -> + syntacticBlocks + |> List.collect (fun block -> SqlAnalysis.analyzeOperation block connectionString schema) + |> List.distinctBy (fun message -> message.Range) + + for message in messages do + AnsiConsole.MarkupLine("Error [red]{0}[/]", message.Message) + + 0 diff --git a/src/Ubik/Project.fs b/src/Ubik/Project.fs new file mode 100644 index 0000000..cd2d08d --- /dev/null +++ b/src/Ubik/Project.fs @@ -0,0 +1,97 @@ +module Project + +open System +open System.IO +open FSharp.Compiler.SourceCodeServices +open FSharp.Compiler.Text +open Npgsql.FSharp.Analyzers.Core + +let checker = + FSharpChecker.Create( + keepAllBackgroundResolutions = true, + keepAssemblyContents = true, + ImplicitlyStartBackgroundWork = true) + +let dumpOpts (opts : FSharpProjectOptions) = + printfn "FSharpProjectOptions.OtherOptions ->" + opts.OtherOptions + |> Array.iter(printfn "%s") + +let loadProject file = + async { + let! source = IO.File.ReadAllTextAsync file |> Async.AwaitTask + let! (opts, error) = checker.GetProjectOptionsFromScript(file, SourceText.ofString source, assumeDotNetFramework = false, useSdkRefs = true, useFsiAuxLib = true, otherFlags = [|"--targetprofile:netstandard" |]) + let newOO = + opts.OtherOptions + |> Array.map(fun i -> + if i.StartsWith("-r:") then + let path = i.Split("-r:", StringSplitOptions.RemoveEmptyEntries).[0] + + sprintf "-r:%s" (IO.FileInfo(path).FullName) + else + i + ) + // dumpOpts opts + return file, opts + } |> Async.RunSynchronously + +let typeCheckFile (file,opts) = + let text = File.ReadAllText file + let st = SourceText.ofString text + let (parseRes, checkAnswer) = + checker.ParseAndCheckFileInProject(file, 1, st, opts) + |> Async.RunSynchronously + + match checkAnswer with + | FSharpCheckFileAnswer.Aborted -> + printfn "Checking of file %s aborted because %A" file parseRes.Errors + None + | FSharpCheckFileAnswer.Succeeded(c) -> + Some (file, text, parseRes, c) + +let entityCache = EntityCache() + +let getAllEntities (checkResults: FSharpCheckFileResults) (publicOnly: bool) : AssemblySymbol list = + try + let res = [ + yield! AssemblyContentProvider.getAssemblySignatureContent AssemblyContentType.Full checkResults.PartialAssemblySignature + let ctx = checkResults.ProjectContext + let assembliesByFileName = + ctx.GetReferencedAssemblies() + |> Seq.groupBy (fun asm -> asm.FileName) + |> Seq.map (fun (fileName, asms) -> fileName, List.ofSeq asms) + |> Seq.toList + |> List.rev // if mscorlib.dll is the first then FSC raises exception when we try to + // get Content.Entities from it. + + for fileName, signatures in assembliesByFileName do + let contentType = if publicOnly then Public else Full + let content = AssemblyContentProvider.getAssemblyContent entityCache.Locking contentType fileName signatures + yield! content + ] + res + with + | _ -> [] + +let createContext (file, text: string, p: FSharpParseFileResults,c: FSharpCheckFileResults) = + match p.ParseTree with + | Some parseTree -> + let context : SqlAnalyzerContext = { + FileName = file + Content = text.Split([|'\n'|]) + ParseTree = parseTree + Symbols = c.PartialAssemblySignature.Entities |> Seq.toList + } + + Some context + | _ -> + None + +let context proj = + let path = + Path.Combine(Environment.CurrentDirectory, proj) + |> Path.GetFullPath + + loadProject path + |> typeCheckFile + |> Option.bind createContext diff --git a/src/Ubik/Ubik.fsproj b/src/Ubik/Ubik.fsproj new file mode 100644 index 0000000..df7b4b7 --- /dev/null +++ b/src/Ubik/Ubik.fsproj @@ -0,0 +1,23 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + + + + + + + + diff --git a/tests/NpgsqlFSharpAnalyzer.Tests/AssemblyInfo.fs b/tests/NpgsqlFSharpAnalyzer.Tests/AssemblyInfo.fs index 0a403d0..a5473c9 100644 --- a/tests/NpgsqlFSharpAnalyzer.Tests/AssemblyInfo.fs +++ b/tests/NpgsqlFSharpAnalyzer.Tests/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] +[] [] -[] -[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "NpgsqlFSharpAnalyzer.Tests" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.17.0" + let [] AssemblyVersion = "3.18.0" let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.17.0" - let [] AssemblyInformationalVersion = "3.17.0" + let [] AssemblyFileVersion = "3.18.0" + let [] AssemblyInformationalVersion = "3.18.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "985f38c696391f5c8f38f4498e3a59db66ffe3c6" + let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" diff --git a/tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs b/tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs index d25bd32..9d573ef 100644 --- a/tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs +++ b/tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs @@ -57,6 +57,14 @@ let tests = Expect.equal 1 operations.Length "There should be one syntactic block found" } + test "Syntactic analysis: SQL block found from lambda body" { + match context (find "../examples/hashing/syntacticAnalysisFromLambdaBody.fs") with + | None -> failwith "Could not crack project" + | Some context -> + let operations = SyntacticAnalysis.findSqlOperations context + Expect.equal 1 operations.Length "There should be one syntactic block found" + } + test "Syntactic Analysis: reading queries with [] query" { match context (find "../examples/hashing/syntacticAnalysis-literalStrings.fs") with | None -> failwith "Could not crack project" diff --git a/tests/examples/hashing/AssemblyInfo.fs b/tests/examples/hashing/AssemblyInfo.fs index 1f8c355..c68e32c 100644 --- a/tests/examples/hashing/AssemblyInfo.fs +++ b/tests/examples/hashing/AssemblyInfo.fs @@ -4,20 +4,20 @@ open System.Reflection [] [] -[] +[] [] -[] -[] +[] +[] [] -[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "examples" let [] AssemblyProduct = "NpgsqlFSharpAnalyzer" - let [] AssemblyVersion = "3.17.0" + let [] AssemblyVersion = "3.18.0" let [] AssemblyMetadata_ReleaseDate = "2020-09-15T00:00:00.0000000" - let [] AssemblyFileVersion = "3.17.0" - let [] AssemblyInformationalVersion = "3.17.0" + let [] AssemblyFileVersion = "3.18.0" + let [] AssemblyInformationalVersion = "3.18.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "985f38c696391f5c8f38f4498e3a59db66ffe3c6" + let [] AssemblyMetadata_GitHash = "8d5412fe3dd28abc45fb45a7d97134d09ec1ce82" diff --git a/tests/examples/hashing/examples.fsproj b/tests/examples/hashing/examples.fsproj index c0cf653..6e4c3cd 100644 --- a/tests/examples/hashing/examples.fsproj +++ b/tests/examples/hashing/examples.fsproj @@ -5,6 +5,7 @@ + diff --git a/tests/examples/hashing/syntacticAnalysisFromLambdaBody.fs b/tests/examples/hashing/syntacticAnalysisFromLambdaBody.fs new file mode 100644 index 0000000..bd5ab62 --- /dev/null +++ b/tests/examples/hashing/syntacticAnalysisFromLambdaBody.fs @@ -0,0 +1,11 @@ +module SyntacticAnalysisFromLambdaBody + +open Npgsql.FSharp + +let getData = + fun (connectionSring: string) -> + connectionSring + |> Sql.connect + |> Sql.query "SELECT * FROM users WHERE user_id = @user_id" + |> Sql.parameters [ "@user_id", Sql.int 42 ] + |> Sql.execute (fun read -> read.int "user_id")