diff --git a/README.md b/README.md index d3c3345..ec0fee8 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,38 @@ Scribe provides **Quill**, a fluent source builder that eliminates raw `StringBu ## Getting Started -```shell -dotnet add package BulletsForHumanity.Scribe +### With the Scribe SDK (Recommended) + +**1.** Add a `global.json` to your solution root: + +```json +{ + "msbuild-sdks": { + "BulletsForHumanity.Scribe.Sdk": "0.3.0" + } +} +``` + +**2.** Create your analyzer/generator `.csproj`: + +```xml + + + + + + ``` -**Requirements:** netstandard2.0 (required by the Roslyn compiler host). See [Project Setup](docs/project-setup.md) for a complete `.csproj` template. +The SDK handles everything: `netstandard2.0` targeting, `LangVersion`, analyzer packaging, `Stubs.cs` polyfills, and dependency bundling. See [Project Setup](docs/project-setup.md) for details. -### Creating a Generator Project +### Manual Setup (Without SDK) -A minimal generator project that uses Scribe: +```shell +dotnet add package BulletsForHumanity.Scribe +``` + +A minimal generator project without the SDK: ```xml @@ -41,6 +64,8 @@ A minimal generator project that uses Scribe: ``` +See [Project Setup](docs/project-setup.md) for a complete `.csproj` template. + Consumer projects reference the generator via NuGet (or a local package via [LocalDev](#local-development-localdev)): ```xml @@ -151,6 +176,7 @@ See [Project Setup & Infrastructure](docs/project-setup.md#local-development-loc | Component | Purpose | |-----------|---------| +| [**Scribe SDK**](docs/project-setup.md#scribe-sdk-recommended) | MSBuild SDK — zero-config project setup for analyzer/generator projects | | [**Quill**](docs/quill-reference.md) | Fluent source builder — indentation, usings, namespaces, XML docs, type resolution | | **ScribeHeader** | Assembly-level attribute for branding generated files with a decorative page header | | **Template** | Minimal `{{key}}` marker substitution for structural shells | diff --git a/Scribe.Sdk/README.md b/Scribe.Sdk/README.md new file mode 100644 index 0000000..ddc482a --- /dev/null +++ b/Scribe.Sdk/README.md @@ -0,0 +1,87 @@ +# BulletsForHumanity.Scribe.Sdk + +A custom MSBuild SDK that eliminates boilerplate from Roslyn analyzer and source generator projects. + +## Quick Start + +**1. Add a `global.json`** with the SDK version: + +```json +{ + "msbuild-sdks": { + "BulletsForHumanity.Scribe.Sdk": "0.3.0" + } +} +``` + +**2. Use the SDK** in your `.csproj`: + +```xml + + + + + + +``` + +That's it. The SDK handles everything else: + +- `TargetFramework=netstandard2.0` (required by Roslyn compiler host) +- `LangVersion=14` for modern C# features +- `EnforceExtendedAnalyzerRules=true` to catch common mistakes +- Analyzer packaging (`analyzers/dotnet/cs/` placement, embedded PDB) +- Private dependency bundling (Scribe DLL alongside your analyzer) +- `Stubs.cs` polyfills for `init`, `record`, `required`, nullable annotations +- LocalDev multi-repo development infrastructure + +## What the SDK Sets + +| Property | Default | Purpose | +|----------|---------|---------| +| `TargetFramework` | `netstandard2.0` | Required by the Roslyn compiler host | +| `LangVersion` | `14` | Modern C# features with polyfill stubs | +| `EnforceExtendedAnalyzerRules` | `true` | Catches analyzer authoring mistakes | +| `IncludeBuildOutput` | `false` | DLL goes into `analyzers/`, not `lib/` | +| `PackageType` | `Analyzer` | Marks package as an analyzer | +| `IncludeSymbols` | `false` | PDB is embedded in the DLL | +| `DebugType` | `embedded` | IDE debugging support | +| `Nullable` | `enable` | Null-safety | +| `GenerateDocumentationFile` | `false` | Not needed for bundled DLLs | +| `CopyLocalLockFileAssemblies` | `true` | Enables private dependency bundling | + +All defaults are set in the early phase (`Sdk.props`) and can be overridden in your `.csproj`. Packaging targets (analyzer DLL placement, dependency bundling) are enforced by the SDK and cannot be overridden. + +## Stubs.cs Polyfills + +The SDK auto-includes `Stubs.cs` with polyfill types for modern C# features on netstandard2.0: + +- `init` setters / `record` types (C# 9) +- `[ModuleInitializer]`, `[SkipLocalsInit]` (C# 9) +- `[CallerArgumentExpression]` (C# 10) +- Interpolated string handlers (C# 10) +- `required` members (C# 11) +- `scoped ref` parameters (C# 11) +- Nullable flow analysis attributes + +All polyfills are guarded by `#if !NET5_0_OR_GREATER` and deactivate on modern targets. + +**Opt out:** Set `false` if you provide your own. + +## Packaging + +`dotnet pack` produces a correct analyzer NuGet package: + +- Your analyzer DLL is placed in `analyzers/dotnet/cs/` +- Private dependencies (like Scribe) are bundled alongside +- Roslyn SDK DLLs are excluded (provided by the compiler host) + +## LocalDev + +The SDK includes [LocalDev](https://github.com/BulletsForHumanity/Scribe/blob/master/docs/project-setup.md#local-development-localdev) infrastructure for multi-repo development. See the main Scribe documentation for setup instructions. + +## Links + +- [Scribe Documentation](https://github.com/BulletsForHumanity/Scribe) +- [Project Setup Guide](https://github.com/BulletsForHumanity/Scribe/blob/master/docs/project-setup.md) +- [Writing Generators](https://github.com/BulletsForHumanity/Scribe/blob/master/docs/writing-generators.md) diff --git a/Scribe.Sdk/Scribe.Sdk.csproj b/Scribe.Sdk/Scribe.Sdk.csproj new file mode 100644 index 0000000..89a7dd6 --- /dev/null +++ b/Scribe.Sdk/Scribe.Sdk.csproj @@ -0,0 +1,46 @@ + + + + BulletsForHumanity.Scribe.Sdk + netstandard2.0 + + false + true + false + + $(NoWarn);NU5128;NU5129;NETSDK1212 + + true + MSBuild SDK for Roslyn analyzer and source generator projects. Provides default project configuration, analyzer packaging targets, netstandard2.0 polyfill stubs, and LocalDev multi-repo development infrastructure. Use with: <Project Sdk="BulletsForHumanity.Scribe.Sdk"> + roslyn;analyzer;generator;source-generator;msbuild-sdk;scribe + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Scribe.Sdk/Sdk/Sdk.props b/Scribe.Sdk/Sdk/Sdk.props new file mode 100644 index 0000000..6c231cb --- /dev/null +++ b/Scribe.Sdk/Sdk/Sdk.props @@ -0,0 +1,61 @@ + + + + + + + + + + + netstandard2.0 + + 14 + + true + + false + + Analyzer + + false + embedded + + enable + + false + + $(NoWarn);NU5128 + + true + + + + + + true + + + + + diff --git a/Scribe.Sdk/Sdk/Sdk.targets b/Scribe.Sdk/Sdk/Sdk.targets new file mode 100644 index 0000000..9e0f681 --- /dev/null +++ b/Scribe.Sdk/Sdk/Sdk.targets @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + <_ScribeSdkDeps Include="@(ReferenceCopyLocalPaths)" + Condition="'%(ReferenceCopyLocalPaths.NuGetPackageId)' != '' + and '$([System.String]::Copy("%(ReferenceCopyLocalPaths.NuGetPackageId)").StartsWith("Microsoft.CodeAnalysis"))' != 'True' + and '%(Extension)' == '.dll'" /> + + + + + + + diff --git a/Scribe.Sdk/content/Stubs.cs b/Scribe.Sdk/content/Stubs.cs new file mode 100644 index 0000000..7f80565 --- /dev/null +++ b/Scribe.Sdk/content/Stubs.cs @@ -0,0 +1,266 @@ +// Polyfill stubs for modern C# language features on netstandard2.0. +// +// These types are recognised by the compiler by their well-known fully-qualified names. +// Marking them internal prevents conflicts with the real BCL types when the binary is +// loaded in a .NET 5+ host (which already carries the real definitions). +// +// Feature map: +// C# 9 init setters / record types → IsExternalInit +// C# 9 [ModuleInitializer] → ModuleInitializerAttribute +// C# 9 [SkipLocalsInit] → SkipLocalsInitAttribute +// C# 10 CallerArgumentExpression → CallerArgumentExpressionAttribute +// C# 10 Interpolated string handlers → InterpolatedStringHandlerAttribute +// InterpolatedStringHandlerArgumentAttribute +// C# 11 required members → RequiredMemberAttribute +// CompilerFeatureRequiredAttribute +// C# 11 scoped ref parameters → ScopedRefAttribute +// Nullable flow analysis attributes → System.Diagnostics. + +// ── System.Runtime.CompilerServices ────────────────────────────────────────── + +#if !NET5_0_OR_GREATER +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace System.Runtime.CompilerServices +#pragma warning restore IDE0130 // Namespace does not match folder structure +{ + /// + /// Marks the init accessor and is required for record types. + /// The compiler looks for this class by its fully-qualified name. + /// + internal static class IsExternalInit { } + + /// Enables the required modifier on members (C# 11). + [AttributeUsage( + AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Field + | AttributeTargets.Property, + Inherited = false, + AllowMultiple = false + )] + internal sealed class RequiredMemberAttribute : Attribute { } + + /// + /// Companion to . The compiler emits this on + /// every constructor of a type that has required members (C# 11). + /// + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + internal sealed class CompilerFeatureRequiredAttribute : Attribute + { + public CompilerFeatureRequiredAttribute(string featureName) => FeatureName = featureName; + + public string FeatureName { get; } + + /// + /// When , a compiler that does not understand this feature + /// is permitted to ignore it. When (the default), the + /// compiler must reject the construct. + /// + public bool IsOptional { get; init; } + + public const string RefStructs = nameof(RefStructs); + public const string RequiredMembers = nameof(RequiredMembers); + } + + /// Enables [ModuleInitializer] on a static void method (C# 9). + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class ModuleInitializerAttribute : Attribute { } + + /// + /// Suppresses zero-initialisation of locals in the decorated method or type (C# 9). + /// + [AttributeUsage( + AttributeTargets.Module + | AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Interface + | AttributeTargets.Constructor + | AttributeTargets.Method + | AttributeTargets.Property + | AttributeTargets.Event, + Inherited = false + )] + internal sealed class SkipLocalsInitAttribute : Attribute { } + + /// + /// Captures the source-text of the argument passed to a decorated parameter (C# 10). + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class CallerArgumentExpressionAttribute : Attribute + { + public CallerArgumentExpressionAttribute(string parameterName) => + ParameterName = parameterName; + + public string ParameterName { get; } + } + + /// Marks a type as a custom interpolated string handler (C# 10). + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] + internal sealed class InterpolatedStringHandlerAttribute : Attribute { } + + /// + /// Specifies which arguments of a method call are passed to a custom interpolated + /// string handler (C# 10). + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class InterpolatedStringHandlerArgumentAttribute : Attribute + { + public InterpolatedStringHandlerArgumentAttribute(string argument) => + Arguments = [argument]; + + public InterpolatedStringHandlerArgumentAttribute(params string[] arguments) => + Arguments = arguments; + + public string[] Arguments { get; } + } + + /// + /// Indicates that a parameter is scoped — its ref-safety scope does not + /// extend beyond the method boundary (C# 11). + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class ScopedRefAttribute : Attribute { } +} + +// ── System.Diagnostics.CodeAnalysis — nullable flow analysis ───────────────── + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies that an output will not be even if the + /// corresponding type allows it. + /// + [AttributeUsage( + AttributeTargets.Field + | AttributeTargets.Parameter + | AttributeTargets.Property + | AttributeTargets.ReturnValue, + Inherited = false + )] + internal sealed class NotNullAttribute : Attribute { } + + /// + /// Specifies that an output may be even if the + /// corresponding type does not allow it. + /// + [AttributeUsage( + AttributeTargets.Field + | AttributeTargets.Parameter + | AttributeTargets.Property + | AttributeTargets.ReturnValue, + Inherited = false + )] + internal sealed class MaybeNullAttribute : Attribute { } + + /// Specifies that is allowed as an input even if the type does not allow it. + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, + Inherited = false + )] + internal sealed class AllowNullAttribute : Attribute { } + + /// Specifies that is disallowed as an input even if the type allows it. + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, + Inherited = false + )] + internal sealed class DisallowNullAttribute : Attribute { } + + /// + /// Specifies that when the method returns , the + /// decorated parameter will not be . + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + public bool ReturnValue { get; } + } + + /// + /// Specifies that when the method returns , the + /// decorated parameter or return value may be . + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + public bool ReturnValue { get; } + } + + /// + /// Specifies that the return value of the decorated member is non-null when the + /// named parameter is non-null. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute { } + + /// + /// Specifies that the method will not return if the decorated boolean parameter + /// has the specified value. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + public bool ParameterValue { get; } + } + + /// + /// Specifies that the method or property ensures that the listed members are not + /// . + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Property, + Inherited = false, + AllowMultiple = true + )] + internal sealed class MemberNotNullAttribute : Attribute + { + public MemberNotNullAttribute(string member) => Members = [member]; + + public MemberNotNullAttribute(params string[] members) => Members = members; + + public string[] Members { get; } + } + + /// + /// Specifies that the method or property ensures that the listed members are not + /// when returning the specified value. + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Property, + Inherited = false, + AllowMultiple = true + )] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = [member]; + } + + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + public bool ReturnValue { get; } + public string[] Members { get; } + } +} +#endif diff --git a/Scribe.slnx b/Scribe.slnx index 8c7d2b0..ce85d1b 100644 --- a/Scribe.slnx +++ b/Scribe.slnx @@ -1,4 +1,5 @@ + diff --git a/docs/architecture-infrastructure.md b/docs/architecture-infrastructure.md index f6e1e32..29f75ab 100644 --- a/docs/architecture-infrastructure.md +++ b/docs/architecture-infrastructure.md @@ -1,14 +1,68 @@ # How the Infrastructure Works -Internal architecture of Scribe's build infrastructure — the LocalDev system. Read this if you're contributing to Scribe or want to understand the MSBuild mechanics. +Internal architecture of Scribe's build infrastructure — the Scribe SDK and LocalDev system. Read this if you're contributing to Scribe or want to understand the MSBuild mechanics. For setup instructions, see [Project Setup & Infrastructure](project-setup.md). --- -## File Layout +## Scribe SDK -The infrastructure lives in `Scribe/build/` and is shipped inside the NuGet package: +The Scribe SDK (`BulletsForHumanity.Scribe.Sdk`) is a custom MSBuild SDK that wraps `Microsoft.NET.Sdk` and auto-configures all boilerplate for Roslyn analyzer/generator projects. + +### Package Layout + +``` +BulletsForHumanity.Scribe.Sdk.nupkg + Sdk/ + Sdk.props <- Chains to Microsoft.NET.Sdk, sets analyzer defaults + Sdk.targets <- Auto-includes Stubs.cs, defines packaging targets + build/ + Scribe.LocalDev.props <- Shared LocalDev infrastructure (early phase) + Scribe.LocalDev.targets <- Shared LocalDev infrastructure (late phase) + content/ + Stubs.cs <- netstandard2.0 polyfills, injected as Compile item +``` + +### SDK Resolution + +MSBuild resolves custom SDKs from NuGet packages that contain `Sdk/Sdk.props` and/or `Sdk/Sdk.targets`. Consumers declare the SDK version in `global.json`: + +```json +{ + "msbuild-sdks": { + "BulletsForHumanity.Scribe.Sdk": "0.3.0" + } +} +``` + +### Sdk.props (Early Phase) + +Runs before the project file is evaluated. Sets overridable defaults: + +1. **Chains to `Microsoft.NET.Sdk`** via `` +2. **Sets analyzer project defaults**: `TargetFramework=netstandard2.0`, `LangVersion=14`, `EnforceExtendedAnalyzerRules=true`, `IncludeBuildOutput=false`, `PackageType=Analyzer`, embedded PDB, nullable enabled +3. **Initialises `ScribeSdkIncludeStubs`** to `true` (opt-out via `false`) +4. **Imports `Scribe.LocalDev.props`** for sentinel file detection and local NuGet source registration + +All properties can be overridden by the consuming `.csproj` because they're set in the early phase. + +### Sdk.targets (Late Phase) + +Runs after the project file. Enforces packaging behaviour: + +1. **Chains to `Microsoft.NET.Sdk`** targets +2. **Auto-includes `Stubs.cs`** as a `Compile` item with `Link="Generated\Stubs.cs"` (invisible in Solution Explorer). Guarded by `ScribeSdkIncludeStubs=true`. +3. **`_ScribeSdkAddAnalyzerDlls` target**: Places the analyzer DLL into `analyzers/dotnet/cs/` in the NuGet package +4. **`_ScribeSdkAddAnalyzerDependencies` target**: Bundles private NuGet dependencies alongside the analyzer DLL. Excludes `Microsoft.CodeAnalysis.*` DLLs (provided by the compiler host). +5. **Sets `CopyLocalLockFileAssemblies=true`** to enable dependency bundling +6. **Imports `Scribe.LocalDev.targets`** for version override wildcard import + +--- + +## LocalDev File Layout + +The LocalDev infrastructure lives in `Scribe/build/` and is shipped inside both the `BulletsForHumanity.Scribe` NuGet package and the `BulletsForHumanity.Scribe.Sdk` package: ``` Scribe/build/ diff --git a/docs/project-setup.md b/docs/project-setup.md index 2674d54..1287950 100644 --- a/docs/project-setup.md +++ b/docs/project-setup.md @@ -4,7 +4,71 @@ This guide covers how to configure a source generator or analyzer project that u --- -## Generator Project Configuration +## Scribe SDK (Recommended) + +The simplest way to set up a generator or analyzer project is with the **Scribe SDK**. It handles all boilerplate automatically. + +### 1. Add a `global.json` + +```json +{ + "msbuild-sdks": { + "BulletsForHumanity.Scribe.Sdk": "0.3.0" + } +} +``` + +### 2. Use the SDK in your `.csproj` + +```xml + + + + + + +``` + +That's it. The SDK sets all required properties, includes `Stubs.cs` polyfills, and configures analyzer packaging automatically. + +### What the SDK provides + +| Property | Default | Purpose | +|----------|---------|---------| +| `TargetFramework` | `netstandard2.0` | Required by the Roslyn compiler host | +| `LangVersion` | `14` | Modern C# features with polyfill stubs | +| `EnforceExtendedAnalyzerRules` | `true` | Catches analyzer authoring mistakes | +| `IncludeBuildOutput` | `false` | DLL goes into `analyzers/`, not `lib/` | +| `PackageType` | `Analyzer` | Marks package as an analyzer | +| `IncludeSymbols` | `false` | PDB is embedded in the DLL | +| `DebugType` | `embedded` | IDE debugging support | +| `Nullable` | `enable` | Null-safety | +| `CopyLocalLockFileAssemblies` | `true` | Enables private dependency bundling | + +All defaults are set in the early phase (`Sdk.props`) and can be overridden in your `.csproj`. Packaging targets (analyzer DLL placement, dependency bundling) are enforced by the SDK and cannot be overridden. + +### Stubs.cs opt-out + +The SDK auto-includes `Stubs.cs` polyfills. If you provide your own, opt out: + +```xml + + false + +``` + +### Packaging + +`dotnet pack` produces a correct analyzer NuGet package automatically: +- Your analyzer DLL is placed in `analyzers/dotnet/cs/` +- Private dependencies (like Scribe) are bundled alongside +- Roslyn SDK DLLs are excluded (provided by the compiler host) + +--- + +## Manual Configuration (Without SDK) + +If you prefer not to use the SDK, configure your project manually. Roslyn analyzers and source generators must target **netstandard2.0** — this is a hard requirement from the compiler host.