Skip to content

Commit

Permalink
Introducing Ubik v3.18 as a dotnet CLI tool
Browse files Browse the repository at this point in the history
  • Loading branch information
Zaid-Ajaj committed Dec 6, 2020
1 parent d105d7d commit 385311d
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 35 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ Analyzer that provides embedded **SQL syntax analysis** when writing queries usi
- Free (MIT licensed)
- Supports VS Code with [Ionide](https://github.com/ionide/ionide-vscode-fsharp) via F# Analyzers SDK
- Supports Visual Studio
- Supports CLI (via Ubik)

## NuGet

| Package | Stable | Prerelease |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| NpgsqlFSharpAnalyzer | [![NuGet Badge](https://buildstats.info/nuget/NpgsqlFSharpAnalyzer)](https://www.nuget.org/packages/NpgsqlFSharpAnalyzer/) | [![NuGet Badge](https://buildstats.info/nuget/NpgsqlFSharpAnalyzer?includePreReleases=true)](https://www.nuget.org/packages/NpgsqlFSharpAnalyzer/) |

| Ubik | [![NuGet Badge](https://buildstats.info/nuget/Ubik)](https://www.nuget.org/packages/Ubik/) | [![NuGet Badge](https://buildstats.info/nuget/Ubik?includePreReleases=true)](https://www.nuget.org/packages/Ubik/) |

## Using The Analyzer (Visual Studio)

Expand Down Expand Up @@ -73,6 +74,35 @@ Make sure you have these settings in Ionide for FSharp
```
Which instructs Ionide to load the analyzers from the directory of the analyzers into which `NpgsqlFSharpAnalyzer` was installed.

# Using CLI with Ubik

### 1 - Configure the connection string to your development database
The analyzer requires a connection string that points to the database you are developing against. You can configure this connection string by either creating a file called `NPGSQL_FSHARP` (without extension) somewhere next to your F# project or preferably in the root of your workspace. This file should contain that connection string and nothing else. An example of the contents of such file:
```
Host=localhost; Username=postgres; Password=postgres; Database=databaseName
```
> Remember to add an entry in your .gitingore file to make sure you don't commit the connection string to your source version control system.
Another way to configure the connection string is by setting the value of an environment variable named `NPGSQL_FSHARP` that contains the connection string.

The analyzer will try to locate and read the file first, then falls back to using the environment variable.

### 2 - Install Ubik as a dotnet CLI tool
```
dotnet tool install ubik --global
```
### 3 - Run Ubik in the directory of the project you want to analyze
```bash
cd ./path/to/project
ubik

ubik ./path/to/Project.fsproj

ubik ./File1.fs ./AnotherFile.fs

ubik --version
```

### Writing Long Multi-line Queries

When it is not convenient to write a query inline like this:
Expand Down
20 changes: 20 additions & 0 deletions build.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,21 @@ let publishToNuget _ =
if exitCode <> 0
then failwith "Could not publish package"

let packUbik _ =
Shell.cleanDir (__SOURCE_DIRECTORY__ </> "dist")
let args =
[
"pack"
"--configuration Release"
sprintf "--output %s" (__SOURCE_DIRECTORY__ </> "dist")
]

let exitCode = Shell.Exec("dotnet", String.concat " " args, "src" </> "Ubik")
if exitCode <> 0
then failwith "dotnet pack failed"

Target.create "PackUbik" packUbik
Target.create "PublishUbik" publishToNuget

//-----------------------------------------------------------------------------
// Target Declaration
Expand Down Expand Up @@ -352,6 +367,11 @@ Target.create "PackNoTests" dotnetPack
==> "PublishToNuGet"
==> "Release"

"DotnetBuild"
==> "DotnetTest"
==> "PackUbik"
==> "PublishUbik"

"DotnetRestore"
==> "WatchTests"

Expand Down
171 changes: 138 additions & 33 deletions src/Ubik/Program.fs
Original file line number Diff line number Diff line change
@@ -1,57 +1,75 @@
open System
open System.IO
open System.Linq
open Npgsql.FSharp.Analyzers.Core
open Spectre.Console
open System.Xml
open FSharp.Compiler.Text
open FSharp.Data.LiteralProviders

let resolveFile (path: string) =
if Path.IsPathRooted path
then path
else Path.GetFullPath (Path.Combine(Environment.CurrentDirectory, path))

type FilePath =
| File of path: string
| InvalidFile of fileName: string

type CliArgs =
| Version
| InvalidArgs of error:string
| Project of projectPath:string
| Files of fsharpFiles: FilePath[]

let getProject (args: string []) =
try
match args with
| [| |] ->
Directory.GetFiles(Environment.CurrentDirectory, "*.fsproj")
|> Array.tryHead
|> Option.map (fun projectPath -> resolveFile projectPath)
|> Option.map (fun projectPath -> Project(resolveFile projectPath))
|> function
| Some project -> project
| None ->
Directory.GetFiles(Environment.CurrentDirectory, "*.fs")
|> Array.map File
|> Files

| [| "--version" |] -> Version

| multipleArgs ->
let firstArg = multipleArgs.[0]
if firstArg.EndsWith(".fsproj") then
Some (resolveFile firstArg)
else
Directory.GetFiles(resolveFile firstArg, "*.fsproj")
if multipleArgs.Length = 1 && multipleArgs.[0].EndsWith ".fsproj" then
Project (resolveFile multipleArgs.[0])
else if Directory.Exists(resolveFile multipleArgs.[0]) then
Directory.GetFiles(resolveFile multipleArgs.[0], "*.fsproj")
|> Array.tryHead
|> Option.map (fun projectPath -> resolveFile projectPath)
with
| error -> None
|> Option.map (fun projectPath -> Project(resolveFile projectPath))
|> function
| Some project -> project
| None ->
Directory.GetFiles(Environment.CurrentDirectory, "*.fs")
|> Array.map File
|> Files
else
multipleArgs
|> Array.filter (fun file -> file.EndsWith ".fs")
|> Array.map (fun file -> try File (resolveFile file) with _ -> InvalidFile file)
|> Files

[<EntryPoint>]
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)
]
with
| error -> InvalidArgs error.Message

for file in fsharpFiles do
AnsiConsole.MarkupLine("Analyzing file [green]{0}[/]", file)
match Project.context file with
let analyzeFiles (fsharpFiles: FilePath[]) =
let errorCount = ResizeArray()
for fsharpFile in fsharpFiles do
match fsharpFile with
| InvalidFile nonExistingFile ->
AnsiConsole.MarkupLine("Analyzing file did not exist [red]{0}[/]", nonExistingFile)
errorCount.Add(1)
| File fsharpFile ->
AnsiConsole.MarkupLine("Analyzing file [green]{0}[/]", fsharpFile)
match Project.context fsharpFile with
| None -> ()
| Some context ->
let syntacticBlocks = SyntacticAnalysis.findSqlOperations context
Expand All @@ -74,6 +92,93 @@ let main argv =
|> List.distinctBy (fun message -> message.Range)

for message in messages do
AnsiConsole.MarkupLine("Error [red]{0}[/]", message.Message)

let range = message.Range
let source = SourceText.ofString(File.ReadAllText fsharpFile)
if range.StartLine = range.EndLine then
let marker =
source.GetLineString(range.StartLine - 1)
|> Seq.mapi (fun index token ->
if index >= range.StartColumn && index < range.EndColumn
then "[orange1]^[/]"
else " "
)
|> String.concat ""

let original =
source.GetLineString(range.StartLine - 1)
|> Seq.mapi (fun index token ->
if index >= range.StartColumn && index < range.EndColumn
then "[orange1]" + token.ToString().EscapeMarkup() + "[/]"
else token.ToString().EscapeMarkup()
)
|> String.concat ""

let before = (range.StartLine - 1).ToString()
let current = range.StartLine.ToString()
let after = (range.StartLine + 1).ToString()

let maxLength = List.max [ before.Length; current.Length; after.Length ]

AnsiConsole.MarkupLine(" [blue]{0} |[/]", before.PadLeft(maxLength, ' '))
AnsiConsole.MarkupLine(" [blue]{0} |[/] {1}", current.PadLeft(maxLength, ' '), original)
AnsiConsole.MarkupLine(" [blue]{0} |[/] {1}", after.PadLeft(maxLength, ' '), marker)
AnsiConsole.MarkupLine("[orange1]{0}[/]", message.Message)
errorCount.Add(1)
else
let lines = [range.StartLine-1 .. range.EndLine+1]
let maxLength =
lines
|> List.map (fun line -> line.ToString().Length)
|> List.max

for line in lines do
if line = range.StartLine - 1 || line = range.EndLine + 1
then AnsiConsole.MarkupLine(" [blue]{0} |[/] ", line.ToString().PadLeft(maxLength, ' '))
else AnsiConsole.MarkupLine(" [blue]{0} |[/] {1}", line.ToString().PadLeft(maxLength, ' '), source.GetLineString(line - 1).EscapeMarkup())

AnsiConsole.MarkupLine("[orange1]{0}[/]", message.Message)
errorCount.Add(1)
let exitCode = errorCount.Sum()
Console.WriteLine()
if exitCode = 0
then AnsiConsole.MarkupLine("[green]No errors found[/]")
elif exitCode = 1
then AnsiConsole.MarkupLine("[orange1]Found 1 error[/]", exitCode)
else AnsiConsole.MarkupLine("[orange1]Found {0} errors[/]", exitCode)
exitCode

let [<Literal>] projectFile = TextFile<"./Ubik.fsproj">.Text

let projectVersion =
let doc = XmlDocument()
use content = new MemoryStream(Text.Encoding.UTF8.GetBytes projectFile)
doc.Load(content)
doc.GetElementsByTagName("Version").[0].InnerText

[<EntryPoint>]
let main argv =
match getProject argv with
| InvalidArgs error ->
AnsiConsole.MarkupLine("[red]{0}: {1}[/]", "Error occured while reading CLI arguments: ", error)
1

| Version ->
printfn "%s" projectVersion
0

| Files files -> analyzeFiles files

| Project project ->
AnsiConsole.MarkupLine("Analyzing project [blue]{0}[/]", project)

let document = XmlDocument()
document.LoadXml(File.ReadAllText project)

let fsharpFileNodes = document.GetElementsByTagName("Compile")
analyzeFiles [|
for item in 0 .. fsharpFileNodes.Count - 1 ->
let relativePath = fsharpFileNodes.[item].Attributes.["Include"].InnerText
let projectParent = Directory.GetParent project
File(Path.Combine(projectParent.FullName, relativePath))
|]
8 changes: 7 additions & 1 deletion src/Ubik/Ubik.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<ToolCommandName>ubik</ToolCommandName>
<PackAsTool>true</PackAsTool>
<IsPackable>true</IsPackable>
<RollForward>Major</RollForward>
<Version>3.18.0</Version>
<PackageReleaseNotes>Initial release of Ubik</PackageReleaseNotes>
</PropertyGroup>

<ItemGroup>
Expand All @@ -17,7 +23,7 @@
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.32.1" />
<PackageReference Update="FSharp.Core" Version="4.7.2" />

<PackageReference Include="FSharp.Data.LiteralProviders" Version="0.2.7" />
</ItemGroup>

</Project>

0 comments on commit 385311d

Please sign in to comment.