Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Compile Include>` 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 `<Reference>` with HintPath for NuGet Packages
Expand Down Expand Up @@ -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
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Company>Contoso</Company>
<Copyright>Copyright © Contoso 2024</Copyright>
</PropertyGroup>
</Project>
```

**`Directory.Build.targets`** (for targets/t
Identify properties repeated across multiple `

[truncated]
Comment on lines 1077 to 1081

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This auto-generated lock file appears to be accidentally truncated: the "Directory.Build Consolidation" section ends mid-sentence and the literal placeholder text [truncated] is committed. Regenerate this compiled lock (or restore the removed content) so the document is complete and does not contain placeholders.

Copilot uses AI. Check for mistakes.
2 changes: 2 additions & 0 deletions plugins/dotnet-msbuild/skills/msbuild-antipatterns/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Compile Include>` 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 `<Reference>` with HintPath for NuGet Packages
Expand Down
62 changes: 62 additions & 0 deletions tests/dotnet-msbuild/msbuild-antipatterns/eval.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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<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: "fsharp/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 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"

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertions for this scenario don’t actually verify that the .fsproj file ordering was fixed: Domain.fs is already present in the broken fixture, so file_contains: Domain.fs will pass even if no reordering happens (or if the build is made to pass by editing .fs source files instead). Strengthen assertions to check that Domain.fs appears before Services.fs (and ideally that the inverse ordering is not present), and/or assert that no .fs files were modified.

Suggested change
value: "Domain.fs"
value: |
Domain.fs" />
<Compile Include="Services.fs"

Copilot uses AI. Check for mistakes.
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
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>
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
}
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
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<!-- Empty: intentionally short-circuits the parent Directory.Build.props
which exists only as a C# antipattern fixture and would otherwise
leak C#-specific settings (analyzers, TreatWarningsAsErrors) into
these F# fixtures. -->
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project>
<!-- Empty: see Directory.Build.props in this folder for rationale. -->
</Project>
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>
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
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
Loading