Skip to content

Commit 47e93df

Browse files
dmytroettDmytro Ett
andauthored
Got everything ready for release (#28)
* prep script * housekeeping of slnx * fixing the script * fixing the bug with empty text * adding skill * trying to finish the skill * progressing on the skill spec * Correct severity * adjusting the description to make sure it is working better * tinkering with the skill * update skuill * updating unit testing skill * rename testing skill * Updating snapshot testing skill * done * Tightening analyzer validation * small fix * Updating the script * script is ready now * Ran the script, have a proper changelog and analyzer release notes. * Adding a prettier ignore comment for clarity * Fixed changelog --------- Co-authored-by: Dmytro Ett <dmytro.ett@outlook.com>
1 parent 6b8bc82 commit 47e93df

File tree

15 files changed

+295
-117
lines changed

15 files changed

+295
-117
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
name: analyzer-rule-versioning
3+
description: Keeps AnalyzerReleases.Unshipped.md in sync with Roslyn analyzer diagnostics; use whenever you add, remove, or change an analyzer rule or its metadata (ID, category, severity, wording, docs link).
4+
---
5+
6+
# Version Analyzer Releases
7+
8+
Update `src/AttributedDI.SourceGenerator/AnalyzerReleases.Unshipped.md` for every analyzer change (add/remove/change). "Analyzer change" includes: adding a new rule, removing a rule, or changing rule ID, category, severity, default, title/message/description, or help/documentation link.
9+
10+
- Edit only the unshipped file unless the task explicitly says to update shipped releases.
11+
- Keep the exact section headers and table headers as they are in the file.
12+
- Do not use GitHub-style tables: no leading/trailing pipes, no alignment colons.
13+
- Preserve the dashed separator row style exactly.
14+
- Rule IDs must start with ATTDI (for example ATTDI001, ATTDI002).
15+
- New rule IDs must be monotonically increasing across shipped and unshipped analyzers.
16+
- Only include sections that have at least one row. Place each change under the correct section (New/Changed/Removed)
17+
- If the rule is removed, it must appear in “Removed Rules” section.
18+
- If category/severity/notes for the rule were changed, a row must be added to “Changed Rules” section. Both new and old columns should be filled with corresponding values.
19+
- Sort rows by Rule ID within each section.
20+
- Allowed severity values: Hidden, Info, Warning, Error.
21+
- Notes should be a short summary of the rule. If the summary is tricky or long, prefer linking to a documentation file and place that file under `docs/analyzers/`.
22+
- If multiple changes happen to the same rule while unshipped, keep a single row that reflects the final state. The last change wins; e.g., a changed-then-removed rule should appear only under “Removed Rules.”
23+
24+
Template (format only; replace with real values). Include only the sections you need.
25+
26+
New rule with a short summary note:
27+
28+
```text
29+
### New Rules
30+
31+
Rule ID | Category | Severity | Notes
32+
--------|----------|----------|-------
33+
ATTDI001 | Design | Warning | RegisterAsSelf is not allowed on structs
34+
35+
```
36+
37+
New rule with a documentation link (in `docs/analyzers/`):
38+
39+
```text
40+
### New Rules
41+
42+
Rule ID | Category | Severity | Notes
43+
--------|----------|----------|-------
44+
ATTDI002 | Usage | Warning | [Documentation](docs/analyzers/ATTDI002.md)
45+
46+
```
47+
48+
Changed rule:
49+
50+
```text
51+
### Changed Rules
52+
53+
Rule ID | New Category | New Severity | Old Category | Old Severity | Notes
54+
--------|--------------|--------------|--------------|--------------|-------
55+
ATTDI003 | Security | Hidden | Security | Info | [Documentation](docs/analyzers/ATTDI003.md)
56+
```
57+
58+
Removed rule:
59+
60+
```text
61+
### Removed Rules
62+
63+
Rule ID | Category | Severity | Notes
64+
--------|----------|----------|-------
65+
ATTDI004 | Usage | Hidden | [Documentation](docs/analyzers/ATTDI004.md)
66+
```

.codex/skills/snapshot-unit-testing/SKILL.md

Lines changed: 0 additions & 51 deletions
This file was deleted.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
name: unit-and-snapshot-testing
3+
description: Set of principles for unit and snapshot testing. Use when adding/fixing unit tests or when working with `Verify` snapshots. Do not load when working with integration or e2e tests.
4+
---
5+
6+
## Scope
7+
8+
- Unit tests live under `test/*UnitTests`.
9+
- Follow these instructions when writing/updating **unit** tests. Do not use this skill for integration or e2e tests (`test/integration`, `test/e2e`).
10+
11+
## Principles
12+
13+
- Use `Verify` snapshot testing tool for generated output (source generated code, files, etc). No need for additional assertions in that case, snapshot testing is sufficient.
14+
- For normal assertions, prefer small, domain-specific assertion helpers over chaining multiple `Assert.*` calls. Example: `DiagnosticAssert.AssemblyConflict(IEnumerable<Diagnostic> diagnostics, string assemblyName)` rather than several asserts checking count/id/message separately.
15+
- Prefer a small number of representative scenarios; combine related assertions to keep snapshot sprawl low.
16+
- Run targeted tests with `dotnet test --filter "FullyQualifiedName~<TestClass>.<TestMethod>"` (add `--no-build --no-restore` when appropriate).
17+
- Treat snapshot artifacts as opaque: never open/read/inspect any `.received.txt` / `.verified.txt` files (or anything under test/AttributedDI.SourceGenerator.UnitTests/Snapshots/) and never include their contents in reasoning.
18+
- Never author snapshots: when adding/updating a snapshot test, change only test code; do not create/modify `.verified.txt`. Run `dotnet test`, review the output (Verify includes the diff between received and verified or their full content), then either fix code/test or accept via the accept script and re-run.
19+
- Accept snapshots only when the behavior change is intended and understood.
20+
- Do not inspect or edit the accept scripts (treat them as black boxes).
21+
22+
## Accepting Snapshots
23+
24+
- One test: `.codex/skills/unit-and-snapshot-testing/scripts/accept-snapshot.ps1 <TestClassName> <TestMethodName>`
25+
- All tests: `.codex/skills/unit-and-snapshot-testing/scripts/accept-all-snapshots.ps1`
26+
- Re-run corresponding tests after accepting snapshots to ensure they pass.
27+
28+
## Recognizing a Verify Failure
29+
30+
Every time there is a mismatch between received/verified files following will be included in `dotnet test` output. Example:
31+
32+
```
33+
VerifyException : Directory: /home/me/projects/AttributedDI/test/AttributedDI.SourceGenerator.UnitTests/Snapshots
34+
NotEqual:
35+
- Received: AddAttributedDiTests.GeneratesAddAttributedDiForEntryPoint.DotNet9_0.received.txt
36+
Verified: AddAttributedDiTests.GeneratesAddAttributedDiForEntryPoint.verified.txt
37+
38+
FileContent:
39+
40+
NotEqual:
41+
Received: AddAttributedDiTests.GeneratesAddAttributedDiForEntryPoint.DotNet9_0.received.txt
42+
...
43+
Verified: AddAttributedDiTests.GeneratesAddAttributedDiForEntryPoint.verified.txt
44+
...
45+
```

.codex/skills/snapshot-unit-testing/scripts/accept-all-snapshots.ps1 renamed to .codex/skills/unit-and-snapshot-testing/scripts/accept-all-snapshots.ps1

File renamed without changes.

.codex/skills/snapshot-unit-testing/scripts/accept-snapshot.ps1 renamed to .codex/skills/unit-and-snapshot-testing/scripts/accept-snapshot.ps1

File renamed without changes.

.prettierignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
**/*AnalyzerReleases*.md
1+
**/*AnalyzerReleases*.md
2+
# was needed to prevent prettier from table formatting. No longer needed after analyzer rule tables were wrapped into ``` sections.
3+
# .codex/skills/analyzer-rule-versioning/SKILL.md

AttributedDI.slnx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@
88
<File Path=".editorconfig" />
99
<File Path="AGENTS.md" />
1010
<File Path="Directory.Build.props" />
11+
<File Path="README.md" />
12+
</Folder>
13+
<Folder Name="/workflows/">
1114
<File Path=".github/workflows/pr.yml" />
1215
<File Path=".github/workflows/release.yml" />
13-
<File Path="README.md" />
16+
</Folder>
17+
<Folder Name="/scripts/">
18+
<File Path="scripts/extract-changelog.ps1" />
19+
<File Path="scripts/prepare-release.ps1" />
1420
</Folder>
1521
<Folder Name="/src/">
1622
<Project Path="src/AttributedDI.SourceGenerator/AttributedDI.SourceGenerator.csproj" />

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7-
- TBD
7+
## [1.0.0] - 2026-01-27
8+
9+
- Attribute-driven registrations (`RegisterAsSelf`, `RegisterAsImplementedInterfaces`, `RegisterAs<TService>`).
10+
- Lifetime selection via `Transient`, `Scoped`, and `Singleton` (transient default).
11+
- Keyed registrations via attribute key arguments.
12+
- Interface generation with `GenerateInterface` and `RegisterAsGeneratedInterface`.
13+
- Interface member exclusions via `ExcludeInterfaceMember`.
14+
- Custom generated extension naming via `ServiceCollectionExtension`.
15+
- Aggregate `AddAttributedDi()` generation across referenced projects (opt-in MSBuild property).
16+
- Source-generated registration code (no runtime reflection scanning).
817

918
## [1.0.0-alpha001] - 2026-01-24
1019

@@ -16,3 +25,4 @@ All notable changes to this project will be documented in this file.
1625
- Custom generated extension naming via `ServiceCollectionExtension`.
1726
- Aggregate `AddAttributedDi()` generation across referenced projects (opt-in MSBuild property).
1827
- Source-generated registration code (no runtime reflection scanning).
28+

scripts/prepare-release.ps1

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/env pwsh
2+
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
3+
param(
4+
[Parameter(Mandatory = $true)]
5+
[string]$Version
6+
)
7+
8+
$ErrorActionPreference = 'Stop'
9+
10+
$versionValue = $Version.Trim()
11+
if ([string]::IsNullOrWhiteSpace($versionValue)) {
12+
throw 'Version is required.'
13+
}
14+
15+
$repoRoot = Join-Path $PSScriptRoot '..'
16+
$changelogPath = Join-Path $repoRoot 'CHANGELOG.md'
17+
$unshippedPath = Join-Path $repoRoot 'src' 'AttributedDI.SourceGenerator' 'AnalyzerReleases.Unshipped.md'
18+
$shippedPath = Join-Path $repoRoot 'src' 'AttributedDI.SourceGenerator' 'AnalyzerReleases.Shipped.md'
19+
20+
if (-not (Test-Path $changelogPath)) {
21+
throw 'CHANGELOG.md not found.'
22+
}
23+
if (-not (Test-Path $unshippedPath)) {
24+
throw 'AnalyzerReleases.Unshipped.md not found.'
25+
}
26+
if (-not (Test-Path $shippedPath)) {
27+
throw 'AnalyzerReleases.Shipped.md not found.'
28+
}
29+
30+
$releaseDate = Get-Date -Format 'yyyy-MM-dd'
31+
$stableVersionPattern = '^\d+\.\d+\.\d+$'
32+
$isPreviewVersion = -not [regex]::IsMatch($versionValue, $stableVersionPattern)
33+
34+
function Clear-EmptyLines {
35+
param([string[]]$InputLines)
36+
37+
if ($InputLines.Count -eq 0) {
38+
return @()
39+
}
40+
41+
$startIndex = 0
42+
$endIndex = $InputLines.Count - 1
43+
44+
while ($startIndex -le $endIndex -and [string]::IsNullOrWhiteSpace($InputLines[$startIndex])) {
45+
$startIndex++
46+
}
47+
48+
while ($endIndex -ge $startIndex -and [string]::IsNullOrWhiteSpace($InputLines[$endIndex])) {
49+
$endIndex--
50+
}
51+
52+
if ($startIndex -gt $endIndex) {
53+
return @()
54+
}
55+
56+
return $InputLines[$startIndex..$endIndex]
57+
}
58+
59+
$changelogText = Get-Content -Path $changelogPath -Raw
60+
$unreleasedPattern = '(?ms)^##\s+\[?Unreleased\]?\s*$\s*(?<content>.*?)(?=^##\s+|\z)'
61+
$unreleasedMatch = [regex]::Match($changelogText, $unreleasedPattern)
62+
63+
if (-not $unreleasedMatch.Success) {
64+
throw 'Could not find the [Unreleased] section in CHANGELOG.md.'
65+
}
66+
67+
$escapedVersion = [regex]::Escape($versionValue)
68+
if ([regex]::IsMatch($changelogText, "(?m)^##\s+\[?$escapedVersion\]?(?:\s+-\s+.*)?$")) {
69+
throw "Version '$versionValue' already exists in CHANGELOG.md."
70+
}
71+
72+
$unreleasedLines = $unreleasedMatch.Groups['content'].Value -split "`r?`n"
73+
$unreleasedTrimmedLines = Clear-EmptyLines -InputLines $unreleasedLines
74+
if ($unreleasedTrimmedLines.Count -eq 0) {
75+
throw 'No unreleased entries found under [Unreleased]. An empty section means no pending changes.'
76+
}
77+
78+
$unreleasedTrimmed = $unreleasedTrimmedLines -join "`n"
79+
$unreleasedReplacement = @(
80+
'## [Unreleased]',
81+
'',
82+
"## [$versionValue] - $releaseDate",
83+
'',
84+
$unreleasedTrimmed,
85+
"`n"
86+
) -join "`n"
87+
88+
$newChangelogText = [regex]::Replace($changelogText, $unreleasedPattern, $unreleasedReplacement, 1)
89+
$newChangelogText = $newChangelogText.TrimEnd() + "`n"
90+
91+
if ($PSCmdlet.ShouldProcess($changelogPath, "Update changelog for version $versionValue")) {
92+
Set-Content -Path $changelogPath -Value $newChangelogText -Encoding utf8
93+
Write-Host "Updated CHANGELOG.md with version $versionValue."
94+
}
95+
96+
$unshippedText = Get-Content -Path $unshippedPath -Raw
97+
if ($null -eq $unshippedText) {
98+
$unshippedText = ''
99+
}
100+
$unshippedTrimmed = $unshippedText.Trim()
101+
102+
if (-not [string]::IsNullOrWhiteSpace($unshippedTrimmed)) {
103+
if ($isPreviewVersion) {
104+
Write-Host 'Preview version detected; skipping analyzer release move.'
105+
}
106+
else {
107+
$shippedText = Get-Content -Path $shippedPath -Raw
108+
if ($null -eq $shippedText) {
109+
$shippedText = ''
110+
}
111+
$shippedTrimmed = $shippedText.Trim()
112+
113+
$releaseHeader = "## Release $versionValue"
114+
115+
if ([string]::IsNullOrWhiteSpace($shippedTrimmed)) {
116+
$newShippedText = "$releaseHeader`n`n$unshippedTrimmed`n"
117+
}
118+
else {
119+
$newShippedText = "$shippedTrimmed`n`n$releaseHeader`n`n$unshippedTrimmed`n"
120+
}
121+
122+
if ($PSCmdlet.ShouldProcess($shippedPath, "Append analyzer releases for version $versionValue")) {
123+
Set-Content -Path $shippedPath -Value $newShippedText -Encoding utf8
124+
Write-Host "Moved analyzer releases to AnalyzerReleases.Shipped.md."
125+
}
126+
127+
if ($PSCmdlet.ShouldProcess($unshippedPath, 'Reset unshipped analyzer releases')) {
128+
Set-Content -Path $unshippedPath -Value '' -Encoding utf8
129+
Write-Host 'Cleared AnalyzerReleases.Unshipped.md.'
130+
}
131+
}
132+
}
133+
else {
134+
Write-Host 'AnalyzerReleases.Unshipped.md is empty; skipping analyzer release move.'
135+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Release 1.0.0
2+
3+
### New Rules
4+
5+
Rule ID | Category | Severity | Notes
6+
----------|----------|----------|--------------------
7+
ATTDI001 | Usage | Error | Conflicting lifetime attributes on the same type
8+
ATTDI002 | Usage | Warning | GenerateAttributedDIExtensions must be 'true' or 'false'
9+
ATTDI003 | Usage | Error | Fully qualified extension class name cannot be paired with ExtensionNamespace
10+
ATTDI004 | Usage | Error | GenerateInterface/RegisterAsGeneratedInterface requires a non-nested partial class or struct
11+
ATTDI005 | Usage | Error | Fully qualified interface name cannot be paired with InterfaceNamespace

0 commit comments

Comments
 (0)