diff --git a/README.md b/README.md index 5afe711..90ba7a6 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ Analyzer that provides embedded **SQL syntax analysis** when writing queries usi - Detecting parameters with type-mismatch - Verifying the columns being read from the result set and their types - Detecting nullable columns -- Built-in code fixes for the above +- Built-in code fixes and nice error messages - Ability to write multi-line queries in `[]` text and referencing it +- Ability to suppress the warnings when you know better than the analyzer ;) - Free (MIT licensed) - Supports VS Code with [Ionide](https://github.com/ionide/ionide-vscode-fsharp) via F# Analyzers SDK - Supports Visual Studio @@ -107,23 +108,26 @@ let activeUsers (connectionString: string) = ``` Just remember that these `[]` strings have to defined in the same module where the query is written. +### Suppressing the generated warning messages ---- +Use the `Sql.skipAnalysis` function from main library to tell the analyzer to skip the analysis of a code block like this one: +```fs +open Npgsql.FSharp + +let badQuery connection = + connection + |> Sql.query "SELECT * FROM non_existing_table" + |> Sql.skipAnalysis + |> Sql.execute (fun read -> read.int64 "user_id") +``` ### Developing Make sure the following **requirements** are installed on your system: - [dotnet SDK](https://www.microsoft.com/net/download/core) 3.0 or higher -- [Mono](http://www.mono-project.com/) if you're on Linux or macOS. - Postgres database server -or - -- [VSCode Dev Container](https://code.visualstudio.com/docs/remote/containers) - ---- - ### Building diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index eb7a3c9..3441881 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,6 @@ +### 3.13.0 - 2020-09-04 +* The ability to suppress warning messages generated by the analyzer + ### 3.12.1 - 2020-08-31 * Remove NpgsqlFSharpAnalyzer.Core nuget package reference from the analyzer diff --git a/src/FParsec/AssemblyInfo.fs b/src/FParsec/AssemblyInfo.fs index 804c0cf..0615425 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.12.1" - let [] AssemblyMetadata_ReleaseDate = "2020-08-31T00:00:00.0000000" - let [] AssemblyFileVersion = "3.12.1" - let [] AssemblyInformationalVersion = "3.12.1" + let [] AssemblyVersion = "3.13.0" + let [] AssemblyMetadata_ReleaseDate = "2020-09-04T00:00:00.0000000" + let [] AssemblyFileVersion = "3.13.0" + let [] AssemblyInformationalVersion = "3.13.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "b1592a80c9432a89e1edf34d5e8dbcb1e1f84338" + let [] AssemblyMetadata_GitHash = "8510b403b2b0a20a3207cc5c7637d8406f2aaee5" diff --git a/src/NpgsqlFSharpAnalyzer.Core/AssemblyInfo.fs b/src/NpgsqlFSharpAnalyzer.Core/AssemblyInfo.fs index 21ba409..dba4fbb 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.12.1" - let [] AssemblyMetadata_ReleaseDate = "2020-08-31T00:00:00.0000000" - let [] AssemblyFileVersion = "3.12.1" - let [] AssemblyInformationalVersion = "3.12.1" + let [] AssemblyVersion = "3.13.0" + let [] AssemblyMetadata_ReleaseDate = "2020-09-04T00:00:00.0000000" + let [] AssemblyFileVersion = "3.13.0" + let [] AssemblyInformationalVersion = "3.13.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "b1592a80c9432a89e1edf34d5e8dbcb1e1f84338" + let [] AssemblyMetadata_GitHash = "8510b403b2b0a20a3207cc5c7637d8406f2aaee5" diff --git a/src/NpgsqlFSharpAnalyzer.Core/SqlAnalysis.fs b/src/NpgsqlFSharpAnalyzer.Core/SqlAnalysis.fs index 1d984f0..143415e 100644 --- a/src/NpgsqlFSharpAnalyzer.Core/SqlAnalysis.fs +++ b/src/NpgsqlFSharpAnalyzer.Core/SqlAnalysis.fs @@ -929,7 +929,12 @@ module SqlAnalysis = /// Uses database schema that is retrieved once during initialization /// and re-used when analyzing the rest of the Sql operation blocks let analyzeOperation (operation: SqlOperation) (connectionString: string) (schema: InformationSchema.DbSchemaLookups) = - match findQuery operation with + let skipAnalysis = + operation.blocks + |> List.exists (fun block -> block = SqlAnalyzerBlock.SkipAnalysis) + + if skipAnalysis then [ ] + else match findQuery operation with | None -> [ ] | Some (query, queryRange) -> diff --git a/src/NpgsqlFSharpAnalyzer.Core/SyntacticAnalysis.fs b/src/NpgsqlFSharpAnalyzer.Core/SyntacticAnalysis.fs index 3a1686c..01fa210 100644 --- a/src/NpgsqlFSharpAnalyzer.Core/SyntacticAnalysis.fs +++ b/src/NpgsqlFSharpAnalyzer.Core/SyntacticAnalysis.fs @@ -35,6 +35,22 @@ module SyntacticAnalysis = | _ -> None | _ -> None + let (|Applied|_|) = function + | SynExpr.App(atomicFlag, isInfix, funcExpr, argExpr, applicationRange) -> + match argExpr with + | SynExpr.Ident ident -> Some (ident.idText, funcExpr.Range, applicationRange) + | SynExpr.LongIdent(isOptional, longDotId, altName, identRange) -> + match longDotId with + | LongIdentWithDots(listOfIds, ranges) -> + let fullName = + listOfIds + |> List.map (fun id -> id.idText) + |> String.concat "." + + Some (fullName, funcExpr.Range, applicationRange) + | _ -> None + | _ -> None + let (|ParameterTuple|_|) = function | SynExpr.Tuple(isStruct, [ SynExpr.Const(SynConst.String(parameterName, paramRange), constRange); Apply(funcName, exprArgs, funcRange, appRange) ], commaRange, tupleRange) -> Some (parameterName, paramRange, funcName, funcRange, Some appRange) @@ -161,6 +177,15 @@ module SyntacticAnalysis = | _ -> [ ] + let rec findSkipAnalysis expr = + match expr with + | Applied("Sql.skipAnalysis", range, appRange) -> + [ SqlAnalyzerBlock.SkipAnalysis ] + | SynExpr.App(exprAtomic, isInfix, funcExpr, argExpr, range) -> + [ yield! findSkipAnalysis funcExpr; yield! findSkipAnalysis argExpr ] + | _ -> + [ ] + let rec findFunc = function | SqlStoredProcedure (funcName, range) -> [ SqlAnalyzerBlock.StoredProcedure(funcName, range) ] @@ -310,6 +335,7 @@ module SyntacticAnalysis = yield! findQuery funcExpr yield! findParameters funcExpr yield! findFunc funcExpr + yield! findSkipAnalysis funcExpr yield SqlAnalyzerBlock.ReadingColumns columns ] @@ -317,10 +343,12 @@ module SyntacticAnalysis = | Apply(("Sql.execute"|"Sql.executeAsync"|"Sql.executeRow"|"Sql.executeRowAsync"|"Sql.iter"|"Sql.iterAsync"), lambdaExpr, funcRange, appRange) -> let columns = findReadColumnAttempts lambdaExpr + let x = 1 let blocks = [ yield! findQuery funcExpr yield! findParameters funcExpr yield! findFunc funcExpr + yield! findSkipAnalysis funcExpr yield SqlAnalyzerBlock.ReadingColumns columns ] @@ -348,6 +376,7 @@ module SyntacticAnalysis = let blocks = [ yield! findQuery funcExpr + yield! findSkipAnalysis funcExpr yield SqlAnalyzerBlock.Parameters(sqlParameters, range) ] @@ -358,6 +387,18 @@ module SyntacticAnalysis = yield! findFunc funcExpr yield! findQuery funcExpr yield! findParameters funcExpr + yield! findSkipAnalysis funcExpr + ] + + [ { blocks = blocks; range = range; } ] + + | Apply("Sql.skipAnalysis", functionArg, range, appRange) -> + let blocks = [ + yield! findFunc funcExpr + yield! findQuery funcExpr + yield! findParameters funcExpr + yield SqlAnalyzerBlock.SkipAnalysis + yield SqlAnalyzerBlock.ReadingColumns (findReadColumnAttempts funcExpr) ] [ { blocks = blocks; range = range; } ] @@ -367,6 +408,7 @@ module SyntacticAnalysis = yield! findFunc funcExpr yield! findQuery funcExpr yield! findParameters funcExpr + yield! findSkipAnalysis funcExpr yield SqlAnalyzerBlock.ReadingColumns (findReadColumnAttempts funcExpr) ] diff --git a/src/NpgsqlFSharpAnalyzer.Core/Types.fs b/src/NpgsqlFSharpAnalyzer.Core/Types.fs index 587fcdc..91b3b18 100644 --- a/src/NpgsqlFSharpAnalyzer.Core/Types.fs +++ b/src/NpgsqlFSharpAnalyzer.Core/Types.fs @@ -56,6 +56,7 @@ type SqlAnalyzerBlock = | StoredProcedure of string * range | Parameters of UsedParameter list * range | ReadingColumns of ColumnReadAttempt list + | SkipAnalysis type SqlOperation = { blocks : SqlAnalyzerBlock list diff --git a/src/NpgsqlFSharpAnalyzer/AssemblyInfo.fs b/src/NpgsqlFSharpAnalyzer/AssemblyInfo.fs index 222875b..4325bda 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.12.1" - let [] AssemblyMetadata_ReleaseDate = "2020-08-31T00:00:00.0000000" - let [] AssemblyFileVersion = "3.12.1" - let [] AssemblyInformationalVersion = "3.12.1" + let [] AssemblyVersion = "3.13.0" + let [] AssemblyMetadata_ReleaseDate = "2020-09-04T00:00:00.0000000" + let [] AssemblyFileVersion = "3.13.0" + let [] AssemblyInformationalVersion = "3.13.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "b1592a80c9432a89e1edf34d5e8dbcb1e1f84338" + let [] AssemblyMetadata_GitHash = "8510b403b2b0a20a3207cc5c7637d8406f2aaee5" diff --git a/src/NpgsqlFSharpParser/AssemblyInfo.fs b/src/NpgsqlFSharpParser/AssemblyInfo.fs index 36b58ef..9a4e437 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.12.1" - let [] AssemblyMetadata_ReleaseDate = "2020-08-31T00:00:00.0000000" - let [] AssemblyFileVersion = "3.12.1" - let [] AssemblyInformationalVersion = "3.12.1" + let [] AssemblyVersion = "3.13.0" + let [] AssemblyMetadata_ReleaseDate = "2020-09-04T00:00:00.0000000" + let [] AssemblyFileVersion = "3.13.0" + let [] AssemblyInformationalVersion = "3.13.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "b1592a80c9432a89e1edf34d5e8dbcb1e1f84338" + let [] AssemblyMetadata_GitHash = "8510b403b2b0a20a3207cc5c7637d8406f2aaee5" diff --git a/src/NpgsqlFSharpParser/Parser.fs b/src/NpgsqlFSharpParser/Parser.fs index 1703e8c..781b44c 100644 --- a/src/NpgsqlFSharpParser/Parser.fs +++ b/src/NpgsqlFSharpParser/Parser.fs @@ -265,6 +265,8 @@ opp.AddOperator(InfixOperator("=", spaces, 9, Associativity.Left, fun left right opp.AddOperator(InfixOperator("<>", spaces, 9, Associativity.Left, fun left right -> Expr.Not(Expr.Equals(left, right)))) opp.AddOperator(InfixOperator("||", spaces, 9, Associativity.Left, fun left right -> Expr.StringConcat(left, right))) opp.AddOperator(InfixOperator("::", spaces, 9, Associativity.Left, fun left right -> Expr.TypeCast(left, right))) +opp.AddOperator(InfixOperator("->>", spaces, 9, Associativity.Left, fun left right -> Expr.JsonIndex(left, right))) + opp.AddOperator(PostfixOperator("IS NULL", spaces, 8, false, fun value -> Expr.Equals(Expr.Null, value))) opp.AddOperator(PostfixOperator("IS NOT NULL", spaces, 8, false, fun value -> Expr.Not(Expr.Equals(Expr.Null, value)))) diff --git a/src/NpgsqlFSharpParser/Types.fs b/src/NpgsqlFSharpParser/Types.fs index 7bd876b..0662511 100644 --- a/src/NpgsqlFSharpParser/Types.fs +++ b/src/NpgsqlFSharpParser/Types.fs @@ -16,6 +16,7 @@ type Expr = | In of left:Expr * right:Expr | As of left:Expr * right:Expr | StringConcat of left:Expr * right:Expr + | JsonIndex of left:Expr * right:Expr | TypeCast of left:Expr * right:Expr | Not of expr:Expr | Equals of left:Expr * right:Expr diff --git a/src/NpgsqlFSharpVs/source.extension.vsixmanifest b/src/NpgsqlFSharpVs/source.extension.vsixmanifest index 823a40a..13e5bb4 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/tests/NpgsqlFSharpAnalyzer.Tests/AssemblyInfo.fs b/tests/NpgsqlFSharpAnalyzer.Tests/AssemblyInfo.fs index 4565326..138e18f 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.12.1" - let [] AssemblyMetadata_ReleaseDate = "2020-08-31T00:00:00.0000000" - let [] AssemblyFileVersion = "3.12.1" - let [] AssemblyInformationalVersion = "3.12.1" + let [] AssemblyVersion = "3.13.0" + let [] AssemblyMetadata_ReleaseDate = "2020-09-04T00:00:00.0000000" + let [] AssemblyFileVersion = "3.13.0" + let [] AssemblyInformationalVersion = "3.13.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "b1592a80c9432a89e1edf34d5e8dbcb1e1f84338" + let [] AssemblyMetadata_GitHash = "8510b403b2b0a20a3207cc5c7637d8406f2aaee5" diff --git a/tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs b/tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs index 18ced81..93ba353 100644 --- a/tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs +++ b/tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs @@ -95,6 +95,34 @@ let tests = failwith "Should not happen" } + test "Syntactic Analysis: skip analysis can be detected" { + match context (find "../examples/hashing/syntaxAnalysis-detectingSkipAnalysis.fs") with + | None -> failwith "Could not crack project" + | Some context -> + match SyntacticAnalysis.findSqlOperations context with + | [ operation ] -> + operation.blocks + |> List.exists (fun block -> block = SqlAnalyzerBlock.SkipAnalysis) + |> fun found -> Expect.isTrue found "Skip analysis block found" + | _ -> + failwith "Should not happen" + } + + test "Semantic analysis: skip analysis doesn't give any errors" { + use db = createTestDatabase() + + match context (find "../examples/hashing/syntaxAnalysis-detectingSkipAnalysis.fs") with + | None -> failwith "Could not crack project" + | Some context -> + match SqlAnalysis.databaseSchema db.ConnectionString with + | Result.Error connectionError -> + failwith connectionError + | Result.Ok schema -> + let block = List.exactlyOne (SyntacticAnalysis.findSqlOperations context) + let messages = SqlAnalysis.analyzeOperation block db.ConnectionString schema + Expect.isEmpty messages "No errors returned" + } + test "Semantic Analysis: parameter type mismatch" { use db = createTestDatabase() @@ -305,6 +333,26 @@ let tests = failwith "Expected only one error message" } + test "SQL query semantic analysis: using dynamically referenced query doesn't give errors" { + use db = createTestDatabase() + + Sql.connect db.ConnectionString + |> Sql.query "CREATE TABLE users (user_id bigserial primary key, username text not null, active bit not null)" + |> Sql.executeNonQuery + |> raiseWhenFailed + + match context (find "../examples/hashing/syntaxAnalysisReferencingQueryDoesNotGiveError.fs") with + | None -> failwith "Could not crack project" + | Some context -> + let block = List.exactlyOne (SyntacticAnalysis.findSqlOperations context) + match SqlAnalysis.databaseSchema db.ConnectionString with + | Result.Error connectionError -> + failwith connectionError + | Result.Ok schema -> + let messages = SqlAnalysis.analyzeOperation block db.ConnectionString schema + Expect.isEmpty messages "There should be no errors" + } + test "SQL query semantic analysis: type mismatch" { use db = createTestDatabase() diff --git a/tests/examples/hashing/AssemblyInfo.fs b/tests/examples/hashing/AssemblyInfo.fs index 61f3713..7bc1269 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.12.1" - let [] AssemblyMetadata_ReleaseDate = "2020-08-31T00:00:00.0000000" - let [] AssemblyFileVersion = "3.12.1" - let [] AssemblyInformationalVersion = "3.12.1" + let [] AssemblyVersion = "3.13.0" + let [] AssemblyMetadata_ReleaseDate = "2020-09-04T00:00:00.0000000" + let [] AssemblyFileVersion = "3.13.0" + let [] AssemblyInformationalVersion = "3.13.0" let [] AssemblyMetadata_ReleaseChannel = "release" - let [] AssemblyMetadata_GitHash = "b1592a80c9432a89e1edf34d5e8dbcb1e1f84338" + let [] AssemblyMetadata_GitHash = "8510b403b2b0a20a3207cc5c7637d8406f2aaee5" diff --git a/tests/examples/hashing/examples.fsproj b/tests/examples/hashing/examples.fsproj index 6bcfccf..3c4d1ba 100644 --- a/tests/examples/hashing/examples.fsproj +++ b/tests/examples/hashing/examples.fsproj @@ -5,6 +5,8 @@ + + @@ -28,7 +30,7 @@ - + diff --git a/tests/examples/hashing/syntaxAnalysis-detectingSkipAnalysis.fs b/tests/examples/hashing/syntaxAnalysis-detectingSkipAnalysis.fs new file mode 100644 index 0000000..6159a23 --- /dev/null +++ b/tests/examples/hashing/syntaxAnalysis-detectingSkipAnalysis.fs @@ -0,0 +1,9 @@ +module SyntaxAnalysisDetectingSkipAnalysis + +open Npgsql.FSharp + +let badQuery connection = + connection + |> Sql.query "SELECT * FROM non_existing_table" + |> Sql.skipAnalysis + |> Sql.execute (fun read -> read.int64 "user_id") diff --git a/tests/examples/hashing/syntaxAnalysisReferencingQueryDoesNotGiveError.fs b/tests/examples/hashing/syntaxAnalysisReferencingQueryDoesNotGiveError.fs new file mode 100644 index 0000000..9e9b3a8 --- /dev/null +++ b/tests/examples/hashing/syntaxAnalysisReferencingQueryDoesNotGiveError.fs @@ -0,0 +1,14 @@ +module SyntaxAnalysisReferencingQueryDoesNotGiveError + +open Npgsql +open Npgsql.FSharp +// this shouldn't be analyzed whe n query is dynamic + +let deleteUsers connection = + + let usersQuery = "DELETE FROM users WHERE user_id = @user_id" + + connection + |> Sql.query usersQuery + |> Sql.parameters [ "@user_id", Sql.int 42 ] + |> Sql.executeNonQuery