diff --git a/agentic-workflows/dotnet-msbuild/shared/compiled/style-and-modernization.lock.md b/agentic-workflows/dotnet-msbuild/shared/compiled/style-and-modernization.lock.md index de1d425429..0b4a218daf 100644 --- a/agentic-workflows/dotnet-msbuild/shared/compiled/style-and-modernization.lock.md +++ b/agentic-workflows/dotnet-msbuild/shared/compiled/style-and-modernization.lock.md @@ -149,6 +149,8 @@ Use this catalog when scanning project files for improvements. **Exception**: Non-SDK-style (legacy) projects require explicit file includes. If migrating, see `msbuild-modernization` skill. +**Exception (F# / `.fsproj`)**: F# compilation is order-dependent — the compiler processes `` items sequentially and a file can only reference types/modules declared in files listed above it. `.fsproj` files must therefore list every source file explicitly, in dependency order (utility/leaf modules at the top, the entry point such as `Program.fs` at the bottom). If a `.fsi` signature file is used, it must appear **immediately before** its companion `.fs` implementation file. + --- ## AP-06: Using `` with HintPath for NuGet Packages @@ -1074,23 +1076,6 @@ Centralizes NuGet version management across a multi-project solution. See [https ## Directory.Build Consolidation -Identify properties repeated across multiple `.csproj` files and move them to shared files. - -**`Directory.Build.props`** (for properties — placed at repo or src root): - -```xml - - - net8.0 - enable - enable - true - Contoso - Copyright © Contoso 2024 - - -``` - -**`Directory.Build.targets`** (for targets/t +Identify properties repeated across multiple ` [truncated] \ No newline at end of file diff --git a/plugins/dotnet-msbuild/skills/msbuild-antipatterns/SKILL.md b/plugins/dotnet-msbuild/skills/msbuild-antipatterns/SKILL.md index a531a7f54f..4c0b6cfaba 100644 --- a/plugins/dotnet-msbuild/skills/msbuild-antipatterns/SKILL.md +++ b/plugins/dotnet-msbuild/skills/msbuild-antipatterns/SKILL.md @@ -152,6 +152,8 @@ Use this catalog when scanning project files for improvements. **Exception**: Non-SDK-style (legacy) projects require explicit file includes. If migrating, see `msbuild-modernization` skill. +**Exception (F# / `.fsproj`)**: F# compilation is order-dependent — the compiler processes `` items sequentially and a file can only reference types/modules declared in files listed above it. `.fsproj` files must therefore list every source file explicitly, in dependency order (utility/leaf modules at the top, the entry point such as `Program.fs` at the bottom). If a `.fsi` signature file is used, it must appear **immediately before** its companion `.fs` implementation file. + --- ## AP-06: Using `` with HintPath for NuGet Packages diff --git a/tests/dotnet-msbuild/msbuild-antipatterns/eval.yaml b/tests/dotnet-msbuild/msbuild-antipatterns/eval.yaml index bab3e0725c..c739d0a476 100644 --- a/tests/dotnet-msbuild/msbuild-antipatterns/eval.yaml +++ b/tests/dotnet-msbuild/msbuild-antipatterns/eval.yaml @@ -15,3 +15,65 @@ scenarios: - "Identified DefineConstants overwrites existing constants instead of appending" - "Identified analyzer PackageReference missing PrivateAssets, unquoted MSBuild condition, mixed Include/Update in same ItemGroup, or Exec instead of Message task" timeout: 160 + + - name: "Add a module to an F# project" + prompt: | + Add a Validation.fs module to the F# project at fsharp/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 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: "fsharp/OrderService/OrderService.fsproj" + value: "Validation.fs" + rubric: + - "Created Validation.fs with a validation function returning Result" + - "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 fsharp/BrokenOrder/ fails to build. Diagnose and fix the issue. + setup: + copy_test_files: true + assertions: + - type: "exit_success" + - type: "file_contains" + path: "fsharp/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 + fsharp/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: "fsharp/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 diff --git a/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/BrokenOrder/BrokenOrder.fsproj b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/BrokenOrder/BrokenOrder.fsproj new file mode 100644 index 0000000000..78119696dc --- /dev/null +++ b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/BrokenOrder/BrokenOrder.fsproj @@ -0,0 +1,11 @@ + + + Exe + net8.0 + + + + + + + diff --git a/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/BrokenOrder/Domain.fs b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/BrokenOrder/Domain.fs new file mode 100644 index 0000000000..3e75356248 --- /dev/null +++ b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/BrokenOrder/Domain.fs @@ -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 +} diff --git a/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/BrokenOrder/Program.fs b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/BrokenOrder/Program.fs new file mode 100644 index 0000000000..fd677e8097 --- /dev/null +++ b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/BrokenOrder/Program.fs @@ -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 diff --git a/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/BrokenOrder/Services.fs b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/BrokenOrder/Services.fs new file mode 100644 index 0000000000..5b4bcbaa37 --- /dev/null +++ b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/BrokenOrder/Services.fs @@ -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 diff --git a/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/Directory.Build.props b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/Directory.Build.props new file mode 100644 index 0000000000..c2880db9d1 --- /dev/null +++ b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/Directory.Build.props @@ -0,0 +1,6 @@ + + + diff --git a/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/Directory.Build.targets b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/Directory.Build.targets new file mode 100644 index 0000000000..8440e88187 --- /dev/null +++ b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/OrderService/Domain.fs b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/OrderService/Domain.fs new file mode 100644 index 0000000000..d7f3263a67 --- /dev/null +++ b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/OrderService/Domain.fs @@ -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 +} diff --git a/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/OrderService/OrderService.fsproj b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/OrderService/OrderService.fsproj new file mode 100644 index 0000000000..dda1176034 --- /dev/null +++ b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/OrderService/OrderService.fsproj @@ -0,0 +1,11 @@ + + + Exe + net8.0 + + + + + + + diff --git a/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/OrderService/Program.fs b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/OrderService/Program.fs new file mode 100644 index 0000000000..2025e9320c --- /dev/null +++ b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/OrderService/Program.fs @@ -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 diff --git a/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/OrderService/Services.fs b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/OrderService/Services.fs new file mode 100644 index 0000000000..ed2963c36c --- /dev/null +++ b/tests/dotnet-msbuild/msbuild-antipatterns/fsharp/OrderService/Services.fs @@ -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