Skip to content

Commit ebf3e89

Browse files
committed
v1.0.0
1 parent 2440d5e commit ebf3e89

9 files changed

Lines changed: 84 additions & 48 deletions

File tree

.github/workflows/release.yml

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ on:
66

77
permissions:
88
contents: write
9+
# Lets the job request the GitHub OIDC token that NuGet/login exchanges for a nuget.org API key.
10+
id-token: write
911

1012
concurrency:
1113
group: ${{ github.workflow }}-${{ github.ref }}
@@ -98,17 +100,24 @@ jobs:
98100
artifacts/unity/*.tgz
99101
if-no-files-found: error
100102

103+
# Trusted Publishing: the job's GitHub OIDC token is exchanged for a short-lived nuget.org API
104+
# key (valid for one hour), so no long-lived key is stored in the repository. nuget.org only
105+
# issues the key if a Trusted Publishing policy on the NUGET_USER account matches this
106+
# repository and the release.yml workflow file.
107+
- name: Log in to nuget.org
108+
if: steps.version.outputs.publish == 'true'
109+
id: nuget-login
110+
uses: NuGet/login@v1
111+
with:
112+
user: ${{ secrets.NUGET_USER }}
113+
101114
# dotnet nuget push uploads the .snupkg sitting next to the .nupkg to the symbol server, so the
102115
# symbols ship with the package without a second push.
103116
- name: Push to nuget.org
104117
if: steps.version.outputs.publish == 'true'
105118
env:
106-
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
119+
NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }}
107120
run: |
108-
if [ -z "$NUGET_API_KEY" ]; then
109-
echo "::error::The NUGET_API_KEY secret is not set."
110-
exit 1
111-
fi
112121
dotnet nuget push "artifacts/nuget/ObjectPoolLinter.${{ steps.version.outputs.version }}.nupkg" \
113122
--api-key "$NUGET_API_KEY" \
114123
--source https://api.nuget.org/v3/index.json \

CHANGELOG.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [v1.0.0] - 2026-09-12
11+
12+
First published release: the `ObjectPoolLinter` package on nuget.org, plus the Unity
13+
`.unitypackage` and UPM `.tgz` attached to the GitHub release. The 0.x versions below were never
14+
published.
15+
16+
### Changed
17+
- The OPL001 message names the allocation consistently and puts it first:
18+
`'new List<int>' allocates inside the frequently-called method 'Update'.` Allocations are named
19+
from the resolved type rather than the source text, so `new System.Collections.Generic.List<int>()`,
20+
`new List<int>()` and `new()` all read `new List<int>`, and arrays read as their type
21+
(`new int[]`) instead of echoing the size (`int[10]`). `Instantiate` calls read
22+
`'Instantiate' allocates inside ...` rather than `'Instantiate' is allocated inside ...`.
23+
Anything that parses the message text needs the new wording; `build/verify-sample.ps1` is updated.
24+
- The message format arguments are in reading order: `{0}` is the allocation, `{1}` the method.
25+
Closes A6.
26+
- OPL001 moved from `AnalyzerReleases.Unshipped.md` to a `Release 1.0.0` section in
27+
`AnalyzerReleases.Shipped.md`.
28+
29+
### Build
30+
- The release workflow publishes to nuget.org with Trusted Publishing instead of the static
31+
`NUGET_API_KEY` secret. `NuGet/login@v1` exchanges the job's GitHub OIDC token for a one-hour API
32+
key, so no long-lived key is stored. The workflow now requests `id-token: write` and reads the
33+
nuget.org profile name from the `NUGET_USER` secret.
34+
1035
## [v0.9.5] - 2026-09-12
1136

1237
### Build
@@ -473,7 +498,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
473498
### Removed
474499
- Empty placeholder test `tests/ObjectPoolLinter.Tests/UnitTest1.cs`.
475500

476-
[Unreleased]: https://github.com/joezhuo2/ObjectPoolLinter/compare/v0.9.5...HEAD
501+
[Unreleased]: https://github.com/joezhuo2/ObjectPoolLinter/compare/v1.0.0...HEAD
502+
[v1.0.0]: https://github.com/joezhuo2/ObjectPoolLinter/compare/v0.9.5...v1.0.0
477503
[v0.9.5]: https://github.com/joezhuo2/ObjectPoolLinter/compare/v0.9.4...v0.9.5
478504
[v0.9.4]: https://github.com/joezhuo2/ObjectPoolLinter/compare/v0.9.3...v0.9.4
479505
[v0.9.3]: https://github.com/joezhuo2/ObjectPoolLinter/compare/v0.9.2...v0.9.3

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ NuGet package and both Unity artifacts. From 1.0.0 on it pushes the package to n
144144
the GitHub release with the artifacts attached. `0.x` tags are dry runs that only upload the files
145145
as a workflow artifact.
146146

147+
Publishing uses nuget.org [Trusted Publishing](https://learn.microsoft.com/nuget/nuget-org/trusted-publishing),
148+
so the repository stores no API key. It needs two things set up once: a Trusted Publishing policy on
149+
nuget.org for this repository and the `release.yml` workflow, and a `NUGET_USER` repository secret
150+
holding the nuget.org profile name (not the email address) that owns the policy.
151+
147152
## Usage
148153

149154
The analyzer runs automatically during build and in IDEs that support Roslyn analyzers (Visual Studio, VS Code with C# Dev Kit, Rider).

build/verify-sample.ps1

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ $ErrorActionPreference = 'Stop'
3030

3131
# One entry per OPL001 warning in samples/SampleUnityCode/SampleBehaviour.cs: '<allocation> in <method>'.
3232
$expected = @(
33-
'System.Collections.Generic.List<int> in Update'
34-
'List<string> in Update'
33+
'new List<int> in Update'
34+
'new List<string> in Update'
3535
'Instantiate in Update'
36-
'int[10] in FixedUpdate'
36+
'new int[] in FixedUpdate'
3737
)
3838

3939
$repoRoot = Split-Path -Parent $PSScriptRoot
@@ -51,7 +51,7 @@ $output | Write-Host
5151
if ($exitCode -ne 0) { throw "Sample build failed with exit code $exitCode." }
5252

5353
# MSBuild can echo a warning more than once; key on file(line,col) to count each diagnostic once.
54-
$pattern = '^(?<location>.+?\(\d+,\d+\)): warning OPL001: ''(?<allocation>.+?)'' is allocated inside the frequently-called method ''(?<method>.+?)''\.'
54+
$pattern = '^(?<location>.+?\(\d+,\d+\)): warning OPL001: ''(?<allocation>.+?)'' allocates inside the frequently-called method ''(?<method>.+?)''\.'
5555
$byLocation = [ordered]@{}
5656
foreach ($line in $output) {
5757
$match = [regex]::Match($line, $pattern)

src/ObjectPoolLinter.Package/ObjectPoolLinter.Package.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
<PropertyGroup>
1111
<PackageId>ObjectPoolLinter</PackageId>
12-
<Version>0.9.5</Version>
12+
<Version>1.0.0</Version>
1313
<Authors>Joe Zhuo</Authors>
1414
<Copyright>Copyright (c) 2026 Joe Zhuo</Copyright>
1515
<Description>Roslyn analyzer for Unity C# that flags object allocations in hot paths and offers code fixes that route them through an object pool, minimizing GC and frame hitches.</Description>
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
; Shipped analyzer releases
22
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3-
;
4-
; No release has shipped yet. The 1.0.0 release commit moves the rules below from
5-
; AnalyzerReleases.Unshipped.md into a "## Release 1.0.0" section here.
3+
4+
## Release 1.0.0
5+
6+
### New Rules
7+
8+
Rule ID | Category | Severity | Notes
9+
--------|----------|----------|-------
10+
OPL001 | Performance | Warning | Detects `new` expressions and Unity `Instantiate` calls inside frequently-invoked methods (e.g. `Update`).
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,2 @@
11
; Unshipped analyzer release
22
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3-
4-
### New Rules
5-
6-
Rule ID | Category | Severity | Notes
7-
--------|----------|----------|-------
8-
OPL001 | Performance | Warning | Detects `new` expressions and Unity `Instantiate` calls inside frequently-invoked methods (e.g. `Update`).

src/ObjectPoolLinter/ObjectPoolAnalyzer.cs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public sealed class ObjectPoolAnalyzer : DiagnosticAnalyzer
1515

1616
private const string Category = "Performance";
1717
private static readonly LocalizableString Title = "Object allocation in hot path";
18-
private static readonly LocalizableString MessageFormat = "'{1}' is allocated inside the frequently-called method '{0}'. Consider using an object pool to avoid per-frame allocations.";
18+
private static readonly LocalizableString MessageFormat = "'{0}' allocates inside the frequently-called method '{1}'. Consider using an object pool to avoid per-frame allocations.";
1919
private static readonly LocalizableString Description = "Allocating objects inside frequently-invoked Unity methods (such as Update) causes garbage collection pressure and frame hitches. Reuse instances via an object pool instead.";
2020

2121
// Points at the default branch rather than a tag: a shipped analyzer keeps linking to the
@@ -111,20 +111,17 @@ internal void AnalyzeAllocation(SyntaxNodeAnalysisContext context)
111111

112112
if (type.IsValueType && type is not IArrayTypeSymbol) return;
113113

114-
string allocatedTypeName = node switch
115-
{
116-
ObjectCreationExpressionSyntax obj => obj.Type.ToString(),
117-
ArrayCreationExpressionSyntax arr => arr.Type.ToString(),
118-
_ => type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)
119-
};
120-
121114
if (TryGetHotPathMethod(node, context.SemanticModel, out var methodName))
122115
{
116+
// Named from the symbol, not the syntax, so `new System.Collections.Generic.List<int>()`,
117+
// `new List<int>()` and `new()` all read `new List<int>`, and `new int[10]` reads `new int[]`.
118+
var allocation = "new " + type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
119+
123120
var diagnostic = Diagnostic.Create(
124121
Rule,
125122
node.GetLocation(),
126-
methodName,
127-
allocatedTypeName
123+
allocation,
124+
methodName
128125
);
129126

130127
context.ReportDiagnostic(diagnostic);
@@ -142,8 +139,8 @@ internal void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
142139
var diagnostic = Diagnostic.Create(
143140
Rule,
144141
invocation.GetLocation(),
145-
methodName,
146-
"Instantiate");
142+
"Instantiate",
143+
methodName);
147144

148145
context.ReportDiagnostic(diagnostic);
149146
}

tests/ObjectPoolLinter.Tests/ObjectPoolAnalyzerTests.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Collections.Generic;
1+
using System.Collections.Generic;
22
using System.Collections.Immutable;
33
using System.Threading.Tasks;
44
using Microsoft.CodeAnalysis;
@@ -89,7 +89,7 @@ void Update()
8989

9090
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
9191
.WithLocation(0)
92-
.WithArguments("Update", "System.Collections.Generic.List<int>");
92+
.WithArguments("new List<int>", "Update");
9393

9494
await VerifyAnalyzerAsync(source, expected);
9595
}
@@ -112,7 +112,7 @@ void Update()
112112

113113
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
114114
.WithLocation(0)
115-
.WithArguments("Update", "Instantiate");
115+
.WithArguments("Instantiate", "Update");
116116

117117
await VerifyAnalyzerAsync(source, expected);
118118
}
@@ -188,7 +188,7 @@ void FixedUpdate()
188188

189189
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
190190
.WithLocation(0)
191-
.WithArguments("FixedUpdate", "System.Collections.Generic.List<int>");
191+
.WithArguments("new List<int>", "FixedUpdate");
192192

193193
await VerifyAnalyzerAsync(source, expected);
194194
}
@@ -232,7 +232,7 @@ void Update()
232232

233233
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
234234
.WithLocation(0)
235-
.WithArguments("Update", "System.Collections.Generic.List<int>");
235+
.WithArguments("new List<int>", "Update");
236236

237237
await VerifyAnalyzerAsync(source, expected);
238238
}
@@ -259,7 +259,7 @@ void Spawn()
259259

260260
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
261261
.WithLocation(0)
262-
.WithArguments("Update", "System.Collections.Generic.List<int>");
262+
.WithArguments("new List<int>", "Update");
263263

264264
await VerifyAnalyzerAsync(source, expected);
265265
}
@@ -390,7 +390,7 @@ void OnTriggerStay(Collider other)
390390

391391
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
392392
.WithLocation(0)
393-
.WithArguments("OnTriggerStay", "System.Collections.Generic.List<int>");
393+
.WithArguments("new List<int>", "OnTriggerStay");
394394

395395
await VerifyAnalyzerAsync(source, expected);
396396
}
@@ -448,7 +448,7 @@ void OnCollisionStay2D(Collision2D collision)
448448

449449
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
450450
.WithLocation(0)
451-
.WithArguments("OnCollisionStay2D", "System.Collections.Generic.List<int>");
451+
.WithArguments("new List<int>", "OnCollisionStay2D");
452452

453453
await VerifyAnalyzerAsync(source, expected);
454454
}
@@ -470,7 +470,7 @@ void OnAnimatorIK(int layerIndex)
470470

471471
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
472472
.WithLocation(0)
473-
.WithArguments("OnAnimatorIK", "System.Collections.Generic.List<int>");
473+
.WithArguments("new List<int>", "OnAnimatorIK");
474474

475475
await VerifyAnalyzerAsync(source, expected);
476476
}
@@ -620,7 +620,7 @@ void Update()
620620

621621
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
622622
.WithLocation(0)
623-
.WithArguments("Update", "System.Collections.Generic.List<int>");
623+
.WithArguments("new List<int>", "Update");
624624

625625
await VerifyWithoutUnityAsync(source, expected);
626626
}
@@ -642,7 +642,7 @@ void Update()
642642

643643
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
644644
.WithLocation(0)
645-
.WithArguments("Update", "int[10]");
645+
.WithArguments("new int[]", "Update");
646646

647647
await VerifyAnalyzerAsync(source, expected);
648648
}
@@ -664,7 +664,7 @@ void Update()
664664

665665
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
666666
.WithLocation(0)
667-
.WithArguments("Update", "int[]");
667+
.WithArguments("new int[]", "Update");
668668

669669
await VerifyAnalyzerAsync(source, expected);
670670
}
@@ -686,7 +686,7 @@ void Update()
686686

687687
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
688688
.WithLocation(0)
689-
.WithArguments("Update", "List<int>");
689+
.WithArguments("new List<int>", "Update");
690690

691691
await VerifyAnalyzerAsync(source, expected);
692692
}
@@ -750,7 +750,7 @@ void Update()
750750

751751
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
752752
.WithLocation(0)
753-
.WithArguments("Update", "System.Collections.Generic.List<int>");
753+
.WithArguments("new List<int>", "Update");
754754

755755
await VerifyAnalyzerAsync(source, expected);
756756
}
@@ -775,7 +775,7 @@ void Update()
775775

776776
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
777777
.WithLocation(0)
778-
.WithArguments("Update", "System.Collections.Generic.List<int>");
778+
.WithArguments("new List<int>", "Update");
779779

780780
await VerifyAnalyzerAsync(source, expected);
781781
}
@@ -840,7 +840,7 @@ public class MyBehaviour : MonoBehaviour
840840

841841
var expected = new DiagnosticResult(ObjectPoolAnalyzer.DiagnosticId, DiagnosticSeverity.Warning)
842842
.WithLocation(0)
843-
.WithArguments(messageName, "System.Collections.Generic.List<int>");
843+
.WithArguments("new List<int>", messageName);
844844

845845
await VerifyAnalyzerAsync(source, expected);
846846
}

0 commit comments

Comments
 (0)