Skip to content

Commit 4435bd2

Browse files
authored
Merge pull request #3215 from nojaf/skills
Add scripts to debug writer events and full format.
2 parents cd2d2b5 + 4eb2aa2 commit 4435bd2

File tree

19 files changed

+361
-67
lines changed

19 files changed

+361
-67
lines changed

.claude/commands/ast.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
description: Parse F# source code to untyped AST
3+
allowed-tools: Bash(dotnet fsi:*), Bash(echo:*)
4+
---
5+
6+
First build the project: `dotnet build src/Fantomas.Core/Fantomas.Core.fsproj`
7+
8+
Then run the AST script. Write the source code to a temp file and pass it as argument:
9+
10+
```
11+
dotnet fsi scripts/ast.fsx <file>
12+
```
13+
14+
Or pipe inline source via stdin:
15+
16+
```
17+
echo '<source>' | dotnet fsi scripts/ast.fsx [--signature]
18+
```
19+
20+
$ARGUMENTS

.claude/commands/format.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
description: Format F# source code using the locally built Fantomas
3+
allowed-tools: Bash(dotnet fsi:*), Bash(echo:*), Bash(dotnet build:*)
4+
---
5+
6+
First build the project: `dotnet build src/Fantomas/Fantomas.fsproj`
7+
8+
Then run the format script. Pass a file path as argument:
9+
10+
```
11+
dotnet fsi scripts/format.fsx [--editorconfig <content>] <file>
12+
```
13+
14+
Or pipe inline source via stdin:
15+
16+
```
17+
echo '<source>' | dotnet fsi scripts/format.fsx [--editorconfig <content>] [--signature]
18+
```
19+
20+
$ARGUMENTS

.claude/commands/oak.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
description: Parse F# source code to Oak (Fantomas intermediate representation)
3+
allowed-tools: Bash(dotnet fsi:*), Bash(echo:*), Bash(dotnet build:*)
4+
---
5+
6+
First build the project: `dotnet build src/Fantomas.Core/Fantomas.Core.fsproj`
7+
8+
Then run the Oak script. Pass a file path as argument:
9+
10+
```
11+
dotnet fsi scripts/oak.fsx <file>
12+
```
13+
14+
Or pipe inline source via stdin:
15+
16+
```
17+
echo '<source>' | dotnet fsi scripts/oak.fsx [--signature]
18+
```
19+
20+
$ARGUMENTS

.claude/commands/writer-events.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
description: Show writer events produced during formatting of F# source code
3+
allowed-tools: Bash(dotnet fsi:*), Bash(echo:*), Bash(dotnet build:*)
4+
---
5+
6+
First build the project: `dotnet build src/Fantomas/Fantomas.fsproj`
7+
8+
Then run the writer-events script. Pass a file path as argument:
9+
10+
```
11+
dotnet fsi scripts/writer-events.fsx [--editorconfig <content>] <file>
12+
```
13+
14+
Or pipe inline source via stdin:
15+
16+
```
17+
echo '<source>' | dotnet fsi scripts/writer-events.fsx [--editorconfig <content>] [--signature]
18+
```
19+
20+
$ARGUMENTS

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,7 @@ tests/.repositories/**
202202
*.sarif
203203

204204
# vscode history plugin
205-
.history/
205+
.history/
206+
207+
# LLM stuff
208+
.claude/settings.local.json

AGENTS.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Fantomas
2+
3+
F# source code formatter. Parses F# to an untyped AST (via vendored FCS), transforms it to an intermediate representation called Oak (`SyntaxOak.fs`), then prints it back via writer events (`CodePrinter.fs` + `Context.fs`).
4+
5+
## Build & Test
6+
7+
```bash
8+
dotnet build fantomas.sln
9+
dotnet test src/Fantomas.Core.Tests/
10+
```
11+
12+
## Diagnostic Scripts
13+
14+
All scripts accept a file path or stdin, with optional `--signature` and `--editorconfig <content>` flags.
15+
16+
- `scripts/ast.fsx` — untyped AST
17+
- `scripts/oak.fsx` — Oak tree
18+
- `scripts/format.fsx` — format with local build
19+
- `scripts/writer-events.fsx` — writer events produced during formatting
20+
21+
Scripts require a debug build first (`dotnet build src/Fantomas/Fantomas.fsproj`).
22+
23+
## Post-task Steps
24+
25+
Run these after completing a task, not during iterative development — analyzers can be slow.
26+
27+
### Format
28+
29+
```bash
30+
dotnet fantomas src docs build.fsx
31+
```
32+
33+
### Analyzers
34+
35+
```bash
36+
dotnet fsi build.fsx -- -p Analyze
37+
```
38+
39+
Output goes to `analysis.sarif` in the repo root.

scripts/ast.fsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
1-
#r "../artifacts/bin/Fantomas.FCS/debug/Fantomas.FCS.dll"
1+
#load "shared.fsx"
22

33
open System.IO
4+
open Shared
5+
6+
let parseAst (input: string) (isSignature: bool) =
7+
try
8+
let ast =
9+
Fantomas.FCS.Parse.parseFile isSignature (Fantomas.FCS.Text.SourceText.ofString input) []
10+
|> fst
11+
12+
$"%A{ast}"
13+
with ex ->
14+
$"Error while parsing AST: %A{ex}"
415

516
match Array.tryHead fsi.CommandLineArgs with
617
| Some scriptPath ->
718
let scriptFile = FileInfo(scriptPath)
819
let sourceFile = FileInfo(Path.Combine(__SOURCE_DIRECTORY__, __SOURCE_FILE__))
920

1021
if scriptFile.FullName = sourceFile.FullName then
11-
let inputPath = fsi.CommandLineArgs.[fsi.CommandLineArgs.Length - 1]
12-
let sample = File.ReadAllText(inputPath)
13-
let isSignature = inputPath.EndsWith(".fsi")
14-
15-
let ast =
16-
Fantomas.FCS.Parse.parseFile isSignature (Fantomas.FCS.Text.SourceText.ofString sample) []
17-
|> fst
18-
19-
ast |> printfn "%A"
20-
| _ -> printfn "Usage: dotnet fsi ast.fsx <input file>"
22+
let sample, isSignature, _ = parseArgs fsi.CommandLineArgs.[1..]
23+
parseAst sample isSignature |> printfn "%s"
24+
| _ -> printfn "Usage: dotnet fsi ast.fsx [--signature] <input file>"

scripts/format.fsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#load "shared.fsx"
2+
3+
open System.IO
4+
open Fantomas.Core
5+
open Shared
6+
7+
let format (input: string) (isSignature: bool) (config: FormatConfig) =
8+
async {
9+
try
10+
let! result = CodeFormatter.FormatDocumentAsync(isSignature, input, config)
11+
return result.Code
12+
with ex ->
13+
return $"Error while formatting: %A{ex}"
14+
}
15+
16+
match Array.tryHead fsi.CommandLineArgs with
17+
| Some scriptPath ->
18+
let scriptFile = FileInfo(scriptPath)
19+
let sourceFile = FileInfo(Path.Combine(__SOURCE_DIRECTORY__, __SOURCE_FILE__))
20+
21+
if scriptFile.FullName = sourceFile.FullName then
22+
let sample, isSignature, config = parseArgs fsi.CommandLineArgs.[1..]
23+
format sample isSignature config |> Async.RunSynchronously |> printfn "%s"
24+
| _ -> printfn "Usage: dotnet fsi format.fsx [--editorconfig <content>] <input file>"

scripts/oak.fsx

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,27 @@
1-
#r "../artifacts/bin/Fantomas.FCS/debug/Fantomas.FCS.dll"
2-
#r "../artifacts/bin/Fantomas.Core/debug/Fantomas.Core.dll"
1+
#load "shared.fsx"
32

4-
open System.Threading.Tasks
3+
open System.IO
54
open Fantomas.Core
5+
open Shared
66

7-
let parseOak (input: string) (isSignature: bool) : Task<string> =
8-
task {
7+
let parseOak (input: string) (isSignature: bool) =
8+
async {
99
try
1010
let! oaks = CodeFormatter.ParseOakAsync(isSignature, input)
1111

1212
match Array.tryHead oaks with
1313
| None -> return "No Oak found in input"
1414
| Some(oak, _) -> return (string oak)
15-
1615
with ex ->
1716
return $"Error while parsing to Oak: %A{ex}"
1817
}
1918

20-
open System.IO
21-
2219
match Array.tryHead fsi.CommandLineArgs with
2320
| Some scriptPath ->
2421
let scriptFile = FileInfo(scriptPath)
2522
let sourceFile = FileInfo(Path.Combine(__SOURCE_DIRECTORY__, __SOURCE_FILE__))
2623

2724
if scriptFile.FullName = sourceFile.FullName then
28-
let inputPath = fsi.CommandLineArgs.[fsi.CommandLineArgs.Length - 1]
29-
let sample = File.ReadAllText(inputPath)
30-
let isSignature = inputPath.EndsWith(".fsi")
31-
32-
parseOak sample isSignature
33-
|> Async.AwaitTask
34-
|> Async.RunSynchronously
35-
|> printfn "%s"
36-
| _ -> printfn "Usage: dotnet fsi oak.fsx <input file>"
25+
let sample, isSignature, _ = parseArgs fsi.CommandLineArgs.[1..]
26+
parseOak sample isSignature |> Async.RunSynchronously |> printfn "%s"
27+
| _ -> printfn "Usage: dotnet fsi oak.fsx [--signature] <input file>"

scripts/shared.fsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#r "../artifacts/bin/Fantomas.FCS/debug/Fantomas.FCS.dll"
2+
#r "../artifacts/bin/Fantomas.Core/debug/Fantomas.Core.dll"
3+
#r "nuget: editorconfig"
4+
5+
#load "../src/Fantomas/EditorConfig.fs"
6+
7+
open System.IO
8+
open Fantomas.Core
9+
open Fantomas.EditorConfig
10+
11+
let parseEditorConfigContent (content: string) : FormatConfig =
12+
let tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())
13+
Directory.CreateDirectory(tempDir) |> ignore
14+
let editorConfigPath = Path.Combine(tempDir, ".editorconfig")
15+
let fsharpFile = Path.Combine(tempDir, "temp.fs")
16+
File.WriteAllText(editorConfigPath, $"root = true\n\n[*.fs]\n%s{content}")
17+
File.WriteAllText(fsharpFile, "")
18+
19+
try
20+
readConfiguration fsharpFile
21+
finally
22+
Directory.Delete(tempDir, true)
23+
24+
/// Parses args and returns (source, isSignature, config).
25+
/// Accepts either a file path as last arg, or source code via stdin.
26+
/// Optional flags: --editorconfig <content>, --signature
27+
let parseArgs (args: string array) =
28+
let editorConfigIdx = args |> Array.tryFindIndex (fun a -> a = "--editorconfig")
29+
let hasSignatureFlag = args |> Array.exists (fun a -> a = "--signature")
30+
31+
let config =
32+
match editorConfigIdx with
33+
| Some idx -> parseEditorConfigContent args.[idx + 1]
34+
| None -> FormatConfig.Default
35+
36+
// Collect flag indices to determine which arg (if any) is the input file
37+
let flagIndices =
38+
[| match editorConfigIdx with
39+
| Some idx ->
40+
yield idx
41+
yield idx + 1
42+
| None -> ()
43+
yield!
44+
args
45+
|> Array.indexed
46+
|> Array.choose (fun (i, a) -> if a = "--signature" then Some i else None) |]
47+
48+
let positionalArgs =
49+
args
50+
|> Array.indexed
51+
|> Array.filter (fun (i, _) -> not (Array.contains i flagIndices))
52+
|> Array.map snd
53+
54+
match Array.tryLast positionalArgs with
55+
| Some path when File.Exists(path) ->
56+
let sample = File.ReadAllText(path)
57+
let isSignature = hasSignatureFlag || path.EndsWith(".fsi")
58+
sample, isSignature, config
59+
| _ ->
60+
let sample = stdin.ReadToEnd()
61+
sample, hasSignatureFlag, config

0 commit comments

Comments
 (0)