|
| 1 | +# Utf8StringInterpolation.Analyzers |
| 2 | + |
| 3 | +[](https://www.nuget.org/packages/Utf8StringInterpolation.Analyzers) |
| 4 | +[](LICENSE) |
| 5 | + |
| 6 | +Roslyn analyzers for [Utf8StringInterpolation](https://github.com/Cysharp/Utf8StringInterpolation) — the zero-allocation UTF-8 string interpolation library by [Cysharp](https://github.com/Cysharp). These analyzers catch usage patterns that silently defeat the library's performance guarantees at compile time, turning invisible runtime regressions into build errors. |
| 7 | + |
| 8 | +## The problem |
| 9 | + |
| 10 | +`Utf8StringInterpolation` achieves zero allocation by making `Utf8StringWriter<T>` an [interpolated string handler](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/interpolated-string-handler). When you call `AppendFormat($"...")`, the C# compiler routes the interpolation directly into the handler — no intermediate `string` is ever created. |
| 11 | + |
| 12 | +However, `Utf8StringWriter<T>` also exposes `Append(string?)`. When you write `Append($"...")`, the compiler sees a plain `string` parameter with no handler attribute, so it **materializes the entire interpolated string as a heap-allocated `string` first**, then passes it in. The code compiles cleanly, produces correct output, and silently allocates — the exact opposite of the library's purpose. |
| 13 | + |
| 14 | +```csharp |
| 15 | +// Looks identical. Behaves very differently. |
| 16 | +zsb.Append($"Hello, {name}!"); // allocates a string — WRONG |
| 17 | +zsb.AppendFormat($"Hello, {name}!"); // zero allocation — correct |
| 18 | +``` |
| 19 | + |
| 20 | +This package makes the wrong form a **build error**. |
| 21 | + |
| 22 | +## Diagnostics |
| 23 | + |
| 24 | +### U8SI001 — Use AppendFormat instead of Append with interpolated strings |
| 25 | + |
| 26 | +**Severity:** Error |
| 27 | +**Category:** Performance |
| 28 | + |
| 29 | +**Before (build error):** |
| 30 | +```csharp |
| 31 | +using Utf8StringInterpolation; |
| 32 | + |
| 33 | +var zsb = Utf8String.CreateWriter(stream); |
| 34 | +zsb.Append($"Hello, {name}!"); // error U8SI001 |
| 35 | +zsb.Append($"You have {count} items."); // error U8SI001 |
| 36 | +``` |
| 37 | + |
| 38 | +**After (zero allocation):** |
| 39 | +```csharp |
| 40 | +zsb.AppendFormat($"Hello, {name}!"); |
| 41 | +zsb.AppendFormat($"You have {count} items."); |
| 42 | +``` |
| 43 | + |
| 44 | +A **code fix** is provided. In Visual Studio, click the lightbulb on any U8SI001 error to replace `Append` with `AppendFormat` automatically. On the command line: |
| 45 | + |
| 46 | +```bash |
| 47 | +dotnet format analyzers <project> --diagnostics U8SI001 --severity error |
| 48 | +``` |
| 49 | + |
| 50 | +> **`Append($"literal")` with no interpolation holes is intentionally ignored.** If the interpolated string contains no `{...}` placeholders, the compiler resolves it to a string constant at compile time — there is no heap allocation at runtime. The diagnostic only fires when actual interpolation holes are present, because those are the cases where `AppendFormat` provides a performance benefit. |
| 51 | +
|
| 52 | +## Installation |
| 53 | + |
| 54 | +```xml |
| 55 | +<PackageReference Include="Utf8StringInterpolation.Analyzers" Version="x.y.z"> |
| 56 | + <PrivateAssets>all</PrivateAssets> |
| 57 | + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |
| 58 | +</PackageReference> |
| 59 | +``` |
| 60 | + |
| 61 | +The package contains both the analyzer and the code fix provider. It has no runtime impact — `PrivateAssets="all"` ensures it is not listed as a dependency of your package. |
| 62 | + |
| 63 | +> **Heads up for existing codebases:** Adding this package to a project that already has `Append($"...")` calls produces immediate build errors. Run `dotnet format analyzers --diagnostics U8SI001 --severity error` on the project to fix existing violations before (or immediately after) adding the `PackageReference`. |
| 64 | +
|
| 65 | +## Compatibility |
| 66 | + |
| 67 | +- C# projects targeting any .NET version (.NET Framework, .NET Core, .NET 5+) |
| 68 | +- Requires the [Utf8StringInterpolation](https://www.nuget.org/packages/Utf8StringInterpolation) NuGet package |
| 69 | +- Tested with Visual Studio, VS Code, and `dotnet build` |
| 70 | + |
| 71 | +## How it works |
| 72 | + |
| 73 | +The analyzer registers on every `InvocationExpression` and checks: |
| 74 | + |
| 75 | +1. The method name is `Append` |
| 76 | +2. At least one argument is an `InterpolatedStringExpressionSyntax` containing at least one interpolation hole (`{...}`) |
| 77 | +3. The receiver type is `Utf8StringInterpolation.Utf8StringWriter<T>` (verified via semantic model, not string matching) |
| 78 | + |
| 79 | +Because `Utf8StringWriter<T>.Append` has no interpolated string handler overload, any `$"..."` argument with holes is necessarily materialized as a heap-allocated `string` by the compiler. Interpolated strings without holes are compile-time constants and are excluded — they have no performance cost. |
| 80 | + |
| 81 | +If all three conditions hold, U8SI001 is reported on the method name. The code fix rewrites the method name to `AppendFormat`, leaving the argument unchanged. |
| 82 | + |
| 83 | +## Related |
| 84 | + |
| 85 | +- [Utf8StringInterpolation](https://github.com/Cysharp/Utf8StringInterpolation) — the library this package guards |
| 86 | +- [ZString](https://github.com/Cysharp/ZString) — the predecessor library (`Utf8StringInterpolation` is its successor) |
| 87 | +- [InterpolatedStringHandler](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/interpolated-string-handler) — the C# feature that makes zero-allocation interpolation possible |
| 88 | + |
| 89 | +## Contributing |
| 90 | + |
| 91 | +Bug reports and pull requests welcome. The repository includes unit tests (Roslyn testing framework) and an integration test that builds a real .NET Framework project, applies the code fix via `dotnet format`, and verifies the result. |
| 92 | + |
| 93 | +## Versioning |
| 94 | + |
| 95 | +Every push to `master` publishes a new patch version automatically via [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning). The version is `major.minor.<commit-height>`. To bump major or minor, edit `version.json`. |
| 96 | + |
| 97 | +## License |
| 98 | + |
| 99 | +[MIT](LICENSE) |
0 commit comments