Skip to content
Merged
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
396 changes: 396 additions & 0 deletions .editorconfig

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/sh

# Git pre-commit hook to prevent committing to main branch
# This script prevents accidental commits to the protected main branch

protected_branch='main'

# Get current branch name
current_branch=$(git rev-parse --abbrev-ref HEAD)

if [ "$current_branch" = "$protected_branch" ]; then
echo "Error: You are trying to commit to the protected branch '$protected_branch'."
echo "Please create a feature branch and work on it instead."
echo ""
echo "To create a feature branch:"
echo " git checkout -b feature/your-feature-name"
echo ""
echo "If you absolutely need to commit to main (not recommended), you can:"
echo " git commit --no-verify"
exit 1
fi

exit 0
24 changes: 24 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/sh

# Git pre-push hook to prevent pushing to main branch
# This script prevents accidental pushes to the protected main branch

protected_branch='main'

while read local_ref local_sha remote_ref remote_sha
do
if [ "$remote_ref" = "refs/heads/$protected_branch" ]; then
echo "Error: You are trying to push to the protected branch '$protected_branch'."
echo "Please create a feature branch and submit a pull request instead."
echo ""
echo "To create a feature branch:"
echo " git checkout -b feature/your-feature-name"
echo " git push origin feature/your-feature-name"
echo ""
echo "If you absolutely need to push to main (not recommended), you can:"
echo " git push --no-verify origin main"
exit 1
fi
done

exit 0
90 changes: 90 additions & 0 deletions .githooks/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Git Hooks Setup

This directory contains custom Git hooks for the repository to enforce code quality and workflow standards.

## Available Hooks

### pre-push
Prevents direct pushes to the `main` branch to enforce a pull request workflow. This helps maintain code quality by ensuring all changes go through code review.

## How to Activate Git Hooks

Git hooks need to be activated manually as they are not automatically enabled when cloning a repository. Follow these steps:

### Method 1: Configure Git Hooks Path (Recommended)

1. Navigate to your repository root directory
2. Run the following command to configure Git to use the `.githooks` directory:
```bash
git config core.hooksPath .githooks
```

### Method 2: Copy Hooks to .git/hooks Directory

1. Navigate to your repository root directory
2. Copy the hook files to the `.git/hooks` directory:

**On Windows (PowerShell):**
```powershell
Copy-Item .githooks\* .git\hooks\ -Force
```

**On macOS/Linux:**
```bash
cp .githooks/* .git/hooks/
chmod +x .git/hooks/*
```

### Verification

To verify that the hooks are properly installed, you can:

1. Check the current hooks path configuration:
```bash
git config core.hooksPath
```

2. List the hooks in your hooks directory:
```bash
ls -la .git/hooks/
# or on Windows:
dir .git\hooks\
```

## Testing the pre-push Hook

To test if the pre-push hook is working:

1. Make sure you're on the `main` branch
2. Try to push directly to main:
```bash
git push origin main
```
3. You should see an error message preventing the push

## Bypassing Hooks (Not Recommended)

If you absolutely need to bypass a hook (use with caution):
```bash
git push --no-verify origin main
```

## Troubleshooting

### Hook Not Executing
- Ensure the hook files have execute permissions (especially on macOS/Linux)
- Verify the hooks path is correctly configured
- Check that the hook files don't have a file extension

### Permission Issues on Windows
- Make sure Git Bash or your terminal has the necessary permissions
- Consider running your terminal as administrator if needed

## Best Practices

1. Always work on feature branches
2. Create pull requests for code review
3. Only merge to main through approved pull requests
4. Keep the main branch stable and deployable

For more information about Git hooks, visit: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
255 changes: 255 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
# GitHub Copilot Instructions - AssemblyMetadata

## Project Overview

**AssemblyMetadata** is a Roslyn **incremental source generator** (NuGet package) that embeds
build-time metadata - ISO 8601 UTC timestamp, Windows FileTime, and individual date/time
components - as `public const` fields directly into any consuming assembly.

Key properties:
- **Zero runtime overhead** - all output members are `const`; the JIT inlines them at every call site
- **No runtime dependency** - the package ships as a Roslyn analyzer; nothing is added to the
consumer's runtime graph
- **Single implementation file** - `src/AssemblyMetadata/AssemblyMetadataGenerator.cs`
- **netstandard2.0 generator, net8/9/10 consumers** - the generator runs inside the Roslyn/MSBuild
host, which requires `netstandard2.0`

---

## Repository Layout

```
src/
AssemblyMetadata/
AssemblyMetadata.csproj # netstandard2.0, IsPackable=true, IncludeBuildOutput=false
AssemblyMetadataGenerator.cs # THE source generator - only file in this project

tests/
AssemblyMetadata.UnitTests/
AssemblyMetadata.UnitTests.csproj
AssemblyMetadataInfoTests.cs # Tests baked-in constants (compile-time output)
AssemblyMetadataGeneratorTests.cs# Tests generator code paths via CSharpGeneratorDriver

sample/
AssemblyMetadata.SampleApp/
Program.cs # Console demo of all generated constants

Directory.Build.props # Shared MSBuild properties (Unio-style)
Directory.Packages.props # Central Package Management (CPM) - all version pins here
global.json # SDK version pin (10.0.103)
Justfile # Task runner (just)
coverlet.runsettings # Code-coverage configuration
NuGet.config # Explicit nuget.org source
.editorconfig # Roslyn/Meziantou code-style rules
```

---

## Core Architecture

### How the Generator Works

```
dotnet build
└─ Roslyn creates new CSharpCompilation
└─ AssemblyMetadataGenerator.Initialize() registers source output
└─ AssemblyMetadataGenerator.Execute() is called
├─ DateTimeOffset.UtcNow captured
└─ AssemblyMetadataGenerator.BuildSource() called
└─ AssemblyMetadataInfo.gen.cs emitted into the compilation
└─ Compiled as internal constants inside the consumer assembly
```

### Generated Output Structure

```csharp
// <auto-generated />
namespace BenjaminAbt.AssemblyMetadata
{
internal static class AssemblyMetadataInfo
{
internal static class BuildInfo
{
public const string BuildTimestamp = "2026-03-02T14:35:07.1234567+00:00";
public const long BuildFileTimeUtc = 133876221071234567L;
public const int BuildDateYear = 2026;
public const int BuildDateMonth = 3;
public const int BuildDateDay = 2;
public const int BuildTimeHour = 14;
public const int BuildTimeMinute = 35;
public const int BuildTimeSecond = 7;
}
}
}
```

---

## Coding Standards

### C# Conventions

- **Language version:** `preview` (latest C# features available)
- **File-scoped namespaces** - always use `namespace Foo.Bar;` not `namespace Foo.Bar { }`
- **Implicit usings** - `enable`; do not add redundant `using System;` etc.
- **Nullable** - `enable`; all reference types annotated
- **`sealed` classes** - prefer `sealed` for all concrete classes unless inheritance is intended
- **`TreatWarningsAsErrors=true`** - all `dotnet build` warnings are hard errors

### Source Generator Rules

- **Always implement `IIncrementalGenerator`** - never use the legacy `ISourceGenerator`
- The `SourceProductionContext.AddSource` hint name must end in `.gen.cs` for tooling recognition
- Use `CultureInfo.InvariantCulture` for all numeric formatting inside `BuildSource`
- Use `DateTimeOffset` (not `DateTime`) so the UTC offset is preserved in the ISO 8601 string
- The generator DLL **must** target `netstandard2.0`; clear global TargetFrameworks with
`<TargetFrameworks />` before re-declaring `<TargetFramework>netstandard2.0</TargetFramework>`
- Set `<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>` on the generator project
- Set `<IncludeBuildOutput>false</IncludeBuildOutput>` - the DLL ships only in
`analyzers/dotnet/cs`, not as a compile-time reference

### Documentation

- Every `public` and `internal` member gets an XML `<summary>`
- Private methods that contain non-trivial logic get full XML docs (`<summary>`, `<param>`,
`<returns>`, `<remarks>`)
- Inline comments explain *why*, not *what*
- All XML docs reference relevant types with `<see cref="..."/>`

---

## Test Conventions

### Two Test Classes - Different Purposes

| Class | What It Tests | Key Tool |
|---|---|---|
| `AssemblyMetadataInfoTests` | Values baked into the compiled binary | Plain xunit assertions on `const` fields |
| `AssemblyMetadataGeneratorTests` | Generator code paths at runtime | `CSharpGeneratorDriver` |

### xunit.v3 MTP Mode

- Framework: **xunit.v3 3.2.2** with Microsoft Testing Platform (MTP)
- `Microsoft.NET.Test.Sdk` is **intentionally excluded** - it generates a competing entry point
- `OutputType=Exe` + `UseAppHost=true` + `CS8892` suppressed in `Directory.Build.props`
- **Run tests with:** `dotnet run --project tests/AssemblyMetadata.UnitTests/...`
- NOT `dotnet test` (vstest infrastructure is incompatible with MTP mode without Test.Sdk)
- Use the Justfile: `just test`

### Writing Generator Tests

Always use `CSharpGeneratorDriver`:

```csharp
private static CSharpCompilation CreateEmptyCompilation() =>
CSharpCompilation.Create(
assemblyName: "TestAssembly",
syntaxTrees: [],
references: [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

[Fact]
public void Generator_ProducesExactlyOneGeneratedFile()
{
var result = CSharpGeneratorDriver.Create(new AssemblyMetadataGenerator())
.RunGenerators(CreateEmptyCompilation())
.GetRunResult();
Assert.Single(result.GeneratedTrees);
}
```

### xunit Assertion Rules

- **xUnit2000**: constant/expected value must be the **first** argument to `Assert.Equal`:
```csharp
// ✅ correct
Assert.Equal(AssemblyMetadataInfo.BuildInfo.BuildDateYear, buildOn.Year);

// ❌ wrong - flips expected/actual in failure messages
Assert.Equal(buildOn.Year, AssemblyMetadataInfo.BuildInfo.BuildDateYear);
```
- Coverage threshold: **90%** (configured in `Directory.Build.props`); aim for 100% on
`AssemblyMetadataGenerator.cs` via driver-based tests

---

## Build System

### Central Package Management

All NuGet versions live in `Directory.Packages.props`. **Never** specify a `Version` attribute
on a `<PackageReference>` in a project file.

```xml
<!-- ✅ correct -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />

<!-- ❌ wrong - version belongs in Directory.Packages.props -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" PrivateAssets="all" />
```

### Common Justfile Commands

```bash
just build # debug build
just test # run all tests (dotnet run)
just test-cov # run tests with coverage report
just pack # create NuGet package
just ci # full pipeline: clean → restore → format-check → build → test
just fmt-build # format + build
```

### Blocked Packages

`Directory.Build.props` has an MSBuild target that hard-errors on:
- `Devlooped.SponsorLink` - telemetry/tracking
- `FluentAssertions` - license concerns; use plain `Assert.*` instead

---

## Adding New Generated Constants

To add a new constant to `BuildInfo`:

1. In `AssemblyMetadataGenerator.cs` → `BuildSource()`: add a new `AppendLine` with the constant
(derive all values solely from the existing `buildOn` parameter - do not call `UtcNow` again)
2. In `AssemblyMetadataGeneratorTests.cs`: add a `[Fact]` verifying the constant is declared
3. In `AssemblyMetadataInfoTests.cs`: add a `[Fact]` verifying the baked-in value is correct
4. Update the API table in `readme.md`

---

## NuGet Package

The package is configured in `Directory.Build.props` (`Label="Package"`) and the generator
csproj. To produce a package:

```bash
just pack
# → ./artifacts/packages/AssemblyMetadata.{version}.nupkg
```

The `.nupkg` contains the generator DLL at `analyzers/dotnet/cs/BenjaminAbt.AssemblyMetadata.dll`
and the logo icon. It includes **no `lib/` folder** (`IncludeBuildOutput=false`).

Consumers must reference it as:
```xml
<PackageReference Include="AssemblyMetadata"
Version="LATEST"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
```

---

## Critical Constraints - Don't Break These

| Constraint | Reason |
|---|---|
| Generator targets `netstandard2.0` | Roslyn/MSBuild host requires it |
| `IncludeBuildOutput=false` | Prevents the generator DLL becoming a runtime reference |
| `EnforceExtendedAnalyzerRules=true` | Required by MSBuild for projects containing analyzers |
| All constants are `const` (not static readonly) | Enables JIT inlining and dead-code elimination |
| All values derived from one `DateTimeOffset` | Guarantees internal consistency of all 8 constants |
| `CultureInfo.InvariantCulture` for all numerics | Prevents locale-dependent source output |
| `DateTimeOffset.UtcNow` (not `DateTime.UtcNow`) | Preserves UTC offset in ISO 8601 string |
Loading