Skip to content

Commit 42dee73

Browse files
authored
Merge pull request #86 from nojaf/scaffold-analyzer
Scaffold analyzer
2 parents ad6752d + c28c440 commit 42dee73

File tree

2 files changed

+210
-7
lines changed

2 files changed

+210
-7
lines changed

build.fsx

+202-7
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
#r "nuget: Humanizer.Core, 2.14.1"
66

77
open System
8-
open System.Text.Json
8+
open System.IO
9+
open System.Xml.Linq
910
open System.Threading
1011
open Fake.IO
1112
open Fake.IO.FileSystemOperators
1213
open Fake.IO.Globbing.Operators
1314
open Fun.Build
14-
open Fun.Build.Internal
1515
open NuGet.Common
1616
open NuGet.Protocol
1717
open NuGet.Protocol.Core.Types
@@ -91,7 +91,7 @@ let getLatestPublishedNugetVersion packageName =
9191
}
9292

9393
let getLatestChangeLogVersion () : SemanticVersion * DateTime * ChangelogData option =
94-
let changelog = System.IO.FileInfo(__SOURCE_DIRECTORY__ </> "CHANGELOG.md")
94+
let changelog = FileInfo(__SOURCE_DIRECTORY__ </> "CHANGELOG.md")
9595
let changeLogResult =
9696
match Parser.parseChangeLog changelog with
9797
| Error error -> failwithf "%A" error
@@ -233,16 +233,16 @@ let mkGitHubRelease
233233
let ghReleaseInfo = mapToGithubRelease currentVersion
234234
let! notes = getReleaseNotes ctx ghReleaseInfo previousReleaseDate
235235
ctx.LogWhenDryRun $"NOTES:\n%s{notes}"
236-
let noteFile = System.IO.Path.GetTempFileName()
237-
System.IO.File.WriteAllText(noteFile, notes)
236+
let noteFile = Path.GetTempFileName()
237+
File.WriteAllText(noteFile, notes)
238238
let file = $"./bin/Ionide.Analyzers.%s{ghReleaseInfo.Version}.nupkg"
239239

240240
let! releaseResult =
241241
ctx.RunCommand
242242
$"gh release create v%s{ghReleaseInfo.Version} {file} --title \"{ghReleaseInfo.Title}\" --notes-file \"{noteFile}\""
243243

244-
if System.IO.File.Exists noteFile then
245-
System.IO.File.Delete(noteFile)
244+
if File.Exists noteFile then
245+
File.Delete(noteFile)
246246

247247
match releaseResult with
248248
| Error _ -> return 1
@@ -315,4 +315,199 @@ pipeline "Release" {
315315
runIfOnlySpecified true
316316
}
317317

318+
let getLastCompileItem (fsproj: string) =
319+
let xml = File.ReadAllText fsproj
320+
let doc = XDocument.Parse xml
321+
doc.Descendants(XName.Get "Compile")
322+
|> Seq.filter (fun xe -> xe.Attribute(XName.Get "Include").Value <> "Program.fs")
323+
|> Seq.last
324+
325+
pipeline "NewAnalyzer" {
326+
stage "Scaffold" {
327+
run (fun _ctx ->
328+
Console.Write "Enter analyzer name: "
329+
let analyzerName = Console.ReadLine().Trim()
330+
331+
let analyzerName =
332+
if analyzerName.EndsWith("Analyzer", StringComparison.Ordinal) then
333+
analyzerName
334+
else
335+
$"%s{analyzerName}Analyzer"
336+
337+
let name = analyzerName.Replace("Analyzer", "").Camelize()
338+
339+
Console.Write("Enter the analyzer Category (existing are \"Suggestion\" or \"Style\"): ")
340+
let category = Console.ReadLine().Trim().Pascalize()
341+
let categoryLowered = category.ToLower()
342+
343+
let number =
344+
Directory.EnumerateFiles(__SOURCE_DIRECTORY__ </> "docs", "*.md", SearchOption.AllDirectories)
345+
|> Seq.choose (fun fileName ->
346+
let name = Path.GetFileNameWithoutExtension(fileName)
347+
match Int32.TryParse(name) with
348+
| true, result -> Some result
349+
| _ -> None
350+
)
351+
|> Seq.max
352+
|> (+) 1
353+
354+
let camelCasedAnalyzerName = analyzerName.Camelize()
355+
356+
let analyzerFile =
357+
__SOURCE_DIRECTORY__
358+
</> $"src/Ionide.Analyzers/%s{category}/%s{analyzerName}.fs"
359+
|> FileInfo
360+
361+
if not analyzerFile.Directory.Exists then
362+
analyzerFile.Directory.Create()
363+
364+
let analyzerContent =
365+
$"""module Ionide.Analyzers.%s{category}.%s{analyzerName}
366+
367+
open FSharp.Compiler.Symbols
368+
open FSharp.Compiler.Text
369+
open FSharp.Compiler.Syntax
370+
open FSharp.Analyzers.SDK
371+
open FSharp.Analyzers.SDK.ASTCollecting
372+
open FSharp.Analyzers.SDK.TASTCollecting
373+
374+
[<Literal>]
375+
let message = "Great message here"
376+
377+
let private analyze () : Message list =
378+
[
379+
{{
380+
Type = "%s{name}"
381+
Message = message
382+
Code = "IONIDE-%03i{number}"
383+
Severity = Severity.Hint
384+
Range = Range.Zero
385+
Fixes = []
386+
}}
387+
]
388+
389+
[<Literal>]
390+
let name = "%s{analyzerName}"
391+
392+
[<Literal>]
393+
let shortDescription =
394+
"Short description about %s{analyzerName}"
395+
396+
[<Literal>]
397+
let helpUri = "https://ionide.io/ionide-analyzers/%s{categoryLowered}/%03i{number}.html"
398+
399+
[<CliAnalyzer(name, shortDescription, helpUri)>]
400+
let %s{name}CliAnalyzer: Analyzer<CliContext> =
401+
fun (context: CliContext) -> async {{ return analyze () }}
402+
403+
[<EditorAnalyzer(name, shortDescription, helpUri)>]
404+
let %s{name}EditorAnalyzer: Analyzer<EditorContext> =
405+
fun (context: EditorContext) -> async {{ return analyze () }}
406+
"""
407+
408+
File.WriteAllText(analyzerFile.FullName, analyzerContent)
409+
printfn $"Created %s{analyzerFile.FullName}"
410+
411+
let addCompileItem relativeFsProj filenameWithoutExtension =
412+
let fsproj = __SOURCE_DIRECTORY__ </> relativeFsProj
413+
let sibling = getLastCompileItem fsproj
414+
415+
if
416+
sibling.Attribute(XName.Get "Include").Value
417+
<> $"%s{filenameWithoutExtension}.fs"
418+
then
419+
sibling.AddAfterSelf(XElement.Parse $"<Compile Include=\"%s{filenameWithoutExtension}.fs\" />")
420+
sibling.Document.Save fsproj
421+
422+
addCompileItem "src/Ionide.Analyzers/Ionide.Analyzers.fsproj" (sprintf "%s\\%s" category analyzerName)
423+
424+
let analyzerTestsFile =
425+
__SOURCE_DIRECTORY__
426+
</> $"tests/Ionide.Analyzers.Tests/%s{category}/%s{analyzerName}Tests.fs"
427+
|> FileInfo
428+
429+
if not analyzerTestsFile.Directory.Exists then
430+
analyzerTestsFile.Directory.Create()
431+
432+
let tripleQuote = "\"\"\""
433+
434+
let analyzerTestsContent =
435+
$"""module Ionide.Analyzers.Tests.%s{category}.%s{analyzerName}Tests
436+
437+
open NUnit.Framework
438+
open FSharp.Compiler.CodeAnalysis
439+
open FSharp.Compiler.Text.Range
440+
open FSharp.Analyzers.SDK
441+
open FSharp.Analyzers.SDK.Testing
442+
open Ionide.Analyzers.%s{category}.%s{analyzerName}
443+
444+
let mutable projectOptions: FSharpProjectOptions = FSharpProjectOptions.zero
445+
446+
[<SetUp>]
447+
let Setup () =
448+
task {{
449+
let! opts = mkOptionsFromProject "net7.0" []
450+
projectOptions <- opts
451+
}}
452+
453+
[<Test>]
454+
let ``first test here`` () =
455+
async {{
456+
let source =
457+
%s{tripleQuote}module Lib
458+
// Some source here
459+
%s{tripleQuote}
460+
461+
let ctx = getContext projectOptions source
462+
let! msgs = %s{name}CliAnalyzer ctx
463+
Assert.That(msgs, Is.Not.Empty)
464+
let msg = msgs[0]
465+
Assert.That(Assert.messageContains message msg, Is.True)
466+
}}
467+
"""
468+
469+
File.WriteAllText(analyzerTestsFile.FullName, analyzerTestsContent)
470+
471+
addCompileItem
472+
"tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj"
473+
$"%s{category}\\%s{analyzerName}Tests"
474+
printfn "Created %s" analyzerTestsFile.FullName
475+
476+
let documentationFile =
477+
__SOURCE_DIRECTORY__ </> $"docs/%s{categoryLowered}/%03i{number}.md" |> FileInfo
478+
479+
if not documentationFile.Directory.Exists then
480+
documentationFile.Directory.Create()
481+
482+
let documentationContent =
483+
$"""---
484+
title: %s{analyzerName}
485+
category: %s{categoryLowered}
486+
categoryindex: 1
487+
index: 1
488+
---
489+
490+
# %s{analyzerName}
491+
492+
## Problem
493+
494+
```fsharp
495+
496+
```
497+
498+
## Fix
499+
500+
```fsharp
501+
502+
```
503+
"""
504+
505+
File.WriteAllText(documentationFile.FullName, documentationContent)
506+
printfn "Created %s, your frontmatter probably isn't correct though." documentationFile.FullName
507+
)
508+
}
509+
510+
runIfOnlySpecified true
511+
}
512+
318513
tryPrintPipelineCommandHelp ()

docs/content/contributions.md

+8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ Or `dotnet fsi build.fsx -- --help` to view non-default pipelines.
3535

3636
### Your analyzer
3737

38+
Scaffold your analyzer by running:
39+
40+
```shell
41+
dotnet fsi .\build.fsx -- -p NewAnalyzer
42+
```
43+
44+
This will prompt you to enter the analyzer's name and category and will create all the necessary files.
45+
3846
We try to split the analyzers up into several categories:
3947

4048
- `hints`

0 commit comments

Comments
 (0)