Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
/plugins/dotnet/skills/csharp-scripts/ @dotnet/roslyn
/tests/dotnet/csharp-scripts/ @dotnet/roslyn

/plugins/dotnet/skills/fsharp-project-structure/ @dotnet/fsharp @T-Gro
/tests/dotnet/fsharp-project-structure/ @dotnet/fsharp @T-Gro

/plugins/dotnet/skills/dotnet-pinvoke/ @dotnet/appmodel
/tests/dotnet/dotnet-pinvoke/ @dotnet/appmodel

Expand Down
65 changes: 65 additions & 0 deletions plugins/dotnet/skills/fsharp-project-structure/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
name: fsharp-project-structure
description: "F# .fsproj file ordering, adding .fs/.fsi files, fixing FS0039 FS0010 FS0034 compilation order errors, signature files."
---

# F# Project File Structure

F# compiles `<Compile Include>` items sequentially, top to bottom. A file can only reference types/modules from files listed **above** it in the .fsproj.

## Inputs

| Input | Required | Description |
|-------|----------|-------------|
| .fsproj file | Yes | The F# project file to modify |

## File compilation order

- File B can use types from file A only if A is listed BEFORE B in the .fsproj
- Entry point (`Program.fs` or `[<EntryPoint>]`) must be LAST
- When adding a file: check its `open` declarations, insert AFTER the last dependency but BEFORE any consumer
- Wrong order symptom: `FS0039 "The type/value/namespace 'X' is not defined"` where X exists in another project file

Example ordering:
```xml
<ItemGroup>
<Compile Include="Domain.fs" />
<Compile Include="Services.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
```

## Signature files (.fsi)

- A `.fsi` defines the public API contract for its companion `.fs` file
- Contains type signatures and `val` declarations — no implementation
- `.fsi` MUST appear immediately BEFORE its `.fs` in the Compile list
- If `.fsi` exists, public members in `.fs` not declared in `.fsi` become internal
- `FS0034` (ValueNotContained): signature and implementation don't match

Example with signatures:
```xml
<ItemGroup>
<Compile Include="Domain.fsi" />
<Compile Include="Domain.fs" />
<Compile Include="Services.fsi" />
<Compile Include="Services.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
```

## Workflow

1. Identify where the new file fits in the dependency chain
2. Add `<Compile Include>` at the correct position in .fsproj
3. Run `dotnet build` to verify

## Pitfalls

| Pitfall | Fix |
|---------|-----|
| New file appended after Program.fs | Insert before Program.fs, after its dependencies |
| `.fsi` placed AFTER its `.fs` | Must be immediately BEFORE |
| Deleting `.fsi` to "fix" FS0034 | Update the `.fsi` to match the new API instead |
| Circular dependency between files | Split one file into two |
| Modifying source to work around order | Reorder `<Compile>` items in .fsproj instead |
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Services.fs" />
<Compile Include="Domain.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
</Project>
15 changes: 15 additions & 0 deletions tests/dotnet/fsharp-project-structure/BrokenOrder/Domain.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module BrokenOrder.Domain

type OrderId = OrderId of int

type OrderItem = {
Name: string
Quantity: int
Price: decimal
}

type Order = {
Id: OrderId
CustomerName: string
Items: OrderItem list
}
14 changes: 14 additions & 0 deletions tests/dotnet/fsharp-project-structure/BrokenOrder/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
open BrokenOrder.Domain
open BrokenOrder.Services

let order = {
Id = OrderId 1
CustomerName = "Alice"
Items = [
{ Name = "Widget"; Quantity = 2; Price = 9.99m }
{ Name = "Gadget"; Quantity = 1; Price = 24.99m }
]
}

let total = processOrder order
printfn "Order total: $%M" total
12 changes: 12 additions & 0 deletions tests/dotnet/fsharp-project-structure/BrokenOrder/Services.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module BrokenOrder.Services

open BrokenOrder.Domain

let calculateTotal (order: Order) =
order.Items
|> List.sumBy (fun item -> item.Price * decimal item.Quantity)

let processOrder (order: Order) =
let total = calculateTotal order
printfn "Processing order for %s: $%M" order.CustomerName total
total
15 changes: 15 additions & 0 deletions tests/dotnet/fsharp-project-structure/OrderService/Domain.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module OrderService.Domain

type OrderId = OrderId of int

type OrderItem = {
Name: string
Quantity: int
Price: decimal
}

type Order = {
Id: OrderId
CustomerName: string
Items: OrderItem list
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Domain.fs" />
<Compile Include="Services.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
</Project>
14 changes: 14 additions & 0 deletions tests/dotnet/fsharp-project-structure/OrderService/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
open OrderService.Domain
open OrderService.Services

let order = {
Id = OrderId 1
CustomerName = "Alice"
Items = [
{ Name = "Widget"; Quantity = 2; Price = 9.99m }
{ Name = "Gadget"; Quantity = 1; Price = 24.99m }
]
}

let total = processOrder order
printfn "Order total: $%M" total
12 changes: 12 additions & 0 deletions tests/dotnet/fsharp-project-structure/OrderService/Services.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module OrderService.Services

open OrderService.Domain

let calculateTotal (order: Order) =
order.Items
|> List.sumBy (fun item -> item.Price * decimal item.Quantity)

let processOrder (order: Order) =
let total = calculateTotal order
printfn "Processing order for %s: $%M" order.CustomerName total
total
61 changes: 61 additions & 0 deletions tests/dotnet/fsharp-project-structure/eval.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
scenarios:
- name: "Add a module to an F# project"
prompt: |
Add a Validation.fs module to the F# project at OrderService/ that validates
orders before processing. It should check that the order has at least one item
and that the customer name is not empty. Return a Result<Order, string> with
a descriptive error message on failure. Then update Program.fs to validate the
order before processing it, printing the error if validation fails.
setup:
copy_test_files: true
assertions:
- type: "exit_success"
- type: "file_contains"
path: "OrderService/OrderService.fsproj"
value: "Validation.fs"
rubric:
- "Created Validation.fs with a validation function returning Result<Order, string>"
- "Inserted Compile Include=\"Validation.fs\" AFTER Domain.fs and BEFORE Program.fs in the .fsproj"
- "Did NOT place Validation.fs after Program.fs in the Compile item list"
- "Updated Program.fs to call the validation function before processing"
- "Ran dotnet build and it succeeded"
timeout: 120

- name: "Fix broken file order causing FS0039"
prompt: |
The F# project at BrokenOrder/ fails to build. Diagnose and fix the issue.
setup:
copy_test_files: true
assertions:
- type: "exit_success"
- type: "file_contains"
path: "BrokenOrder/BrokenOrder.fsproj"
value: "Domain.fs"
rubric:
- "Ran dotnet build and observed FS0039 or similar 'not defined' errors"
- "Identified that the failure is caused by wrong file order in the .fsproj — Services.fs is listed before Domain.fs"
- "Reordered Compile items so Domain.fs appears before Services.fs in the .fsproj"
- "Did NOT modify any .fs source files to work around the ordering issue"
- "Ran dotnet build after the fix and confirmed it succeeds"
timeout: 120

- name: "Add a signature file to define public API"
prompt: |
Add a signature file (Domain.fsi) for the Domain module in the F# project at
OrderService/ to explicitly define its public API surface. The signature should
expose all types and keep the module's public contract clear. Make sure the
project still builds after adding the signature file.
setup:
copy_test_files: true
assertions:
- type: "exit_success"
- type: "file_contains"
path: "OrderService/OrderService.fsproj"
value: "Domain.fsi"
rubric:
- "Created Domain.fsi with type signatures matching the types in Domain.fs"
- "Inserted Compile Include=\"Domain.fsi\" immediately BEFORE Domain.fs in the .fsproj"
- "Did NOT place Domain.fsi after Domain.fs in the Compile item list"
- "The .fsi file contains type declarations (not implementation code)"
- "The project builds successfully with the signature file present"
timeout: 120
Loading