diff --git a/docs/wiki/Analyzers.md b/docs/wiki/Analyzers.md
index a27b86d..740c78c 100644
--- a/docs/wiki/Analyzers.md
+++ b/docs/wiki/Analyzers.md
@@ -51,6 +51,7 @@ The analyzers enforce two families of rules:
| `PSGFR37` | One extension class per receiver type; split classes that extend multiple types. |
| `PSGFR38` | Extension classes should carry `[EditorBrowsable(EditorBrowsableState.Never)]`. |
| `PSGFR39` | A non-packable Roslyn component that explicitly opts out of the default self-contained analyzer output (`PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles=false`) while embedding the framework, otherwise the package embeds the loose framework DLL under `analyzers/`. |
+| `PSGFR40` | In Roslyn components (`IsRoslynComponent=true`), qualify XML doc `cref` references to SGF public types with `global::Purview.SourceGeneratorFramework...`. |
## Type-library and attribute-model diagnostics
@@ -80,6 +81,7 @@ analyzer rules above, including:
- `PreferStructuredCodeWriterIfBlockCodeFixProvider` — rewrites raw `if`/`else if`/`else` block text
to the structured `IfBlock`/`ElseIf`/`Else` APIs (`PSGFR23`).
- `CodeWriterToStringCodeFixProvider` — replaces embedded `CodeWriter` string interpolation (`PSGFR29`).
+- `QualifyFrameworkCrefCodeFixProvider` — rewrites SGF XML doc `cref` targets to fully qualified `global::Purview.SourceGeneratorFramework...` names (`PSGFR40`).
- `AttributeDataModelSymbolPropertyCodeFixProvider` — fixes attribute-data-model symbol properties.
- `ReorganizeExtensionClassCodeFixProvider` — renames (`PSGFR35`), splits multi-receiver classes
(`PSGFR37`), moves the class under `Extensions/{ReceiverNamespace}/`, and updates referencing files
@@ -113,4 +115,4 @@ reload the project for the fixes to appear.
## License
-This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project.
\ No newline at end of file
+This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project.
diff --git a/package.json b/package.json
index ef06027..168f5f4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "purview-sourcegenerator-framework",
- "version": "1.0.0-prerelease.49",
+ "version": "1.0.0-prerelease.50",
"license": "MIT",
"author": {
"name": "Kieron Lanning",
diff --git a/src/src/SourceGeneratorFramework.Analyzers/AmbiguousFrameworkCrefAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/AmbiguousFrameworkCrefAnalyzer.cs
new file mode 100644
index 0000000..19f47aa
--- /dev/null
+++ b/src/src/SourceGeneratorFramework.Analyzers/AmbiguousFrameworkCrefAnalyzer.cs
@@ -0,0 +1,229 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Purview.SourceGeneratorFramework.Analyzers;
+
+///
+/// Flags unqualified XML documentation cref references to SGF public types in Roslyn components.
+/// Qualifying these cref targets avoids the duplicate-framework ambiguity that can arise when the
+/// same SGF type is visible through multiple assembly identities.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class AmbiguousFrameworkCrefAnalyzer : DiagnosticAnalyzer
+{
+ public const string DiagnosticId = "PSGFR40";
+ internal const string QualifiedTypePropertyName = "QualifiedTypeName";
+ const string FrameworkAssemblyName = "Purview.SourceGeneratorFramework";
+
+ static readonly DiagnosticDescriptor Rule = new(
+ DiagnosticId,
+ "Qualify SGF cref with global::",
+ "XML documentation cref '{0}' refers to SGF type '{1}'; qualify the cref with 'global::'",
+ "Purview.SourceGeneratorFramework",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "Detects unqualified XML documentation cref references to Purview.SourceGeneratorFramework public types in Roslyn components so they can be rewritten to a fully qualified global:: name."
+ );
+
+ public override ImmutableArray SupportedDiagnostics => [Rule];
+
+ public override void Initialize(AnalysisContext context)
+ {
+ if (context is null)
+ throw new ArgumentNullException(nameof(context));
+
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterCompilationStartAction(static compilationStartContext =>
+ {
+ var analyzerOptions = compilationStartContext.Options.AnalyzerConfigOptionsProvider.GlobalOptions;
+
+ if (!IsExplicitlyTrue(analyzerOptions, "IsRoslynComponent"))
+ return;
+
+ var frameworkAssembly = ResolveFrameworkAssembly(compilationStartContext.Compilation);
+ if (frameworkAssembly is null)
+ return;
+
+ var publicFrameworkTypes = CollectPublicFrameworkTypes(frameworkAssembly.GlobalNamespace);
+ if (publicFrameworkTypes.IsEmpty)
+ return;
+
+ compilationStartContext.RegisterSemanticModelAction(context =>
+ AnalyzeSemanticModel(context, publicFrameworkTypes)
+ );
+ });
+ }
+
+ static void AnalyzeSemanticModel(
+ SemanticModelAnalysisContext context,
+ ImmutableDictionary publicFrameworkTypes
+ )
+ {
+ var semanticModel = context.SemanticModel;
+ var root = semanticModel.SyntaxTree.GetRoot(context.CancellationToken);
+
+ foreach (var trivia in root.DescendantTrivia(descendIntoTrivia: true))
+ {
+ if (
+ !trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)
+ && !trivia.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia)
+ )
+ {
+ continue;
+ }
+
+ foreach (var cref in trivia.GetStructure()?.DescendantNodesAndSelf().OfType() ?? [])
+ {
+ if (IsAlreadyQualified(cref.Cref))
+ continue;
+
+ if (!TryGetUnqualifiedTypeName(cref.Cref, out var shortTypeName))
+ continue;
+
+ if (!publicFrameworkTypes.TryGetValue(shortTypeName, out var frameworkType))
+ continue;
+
+ if (!ReferencesFrameworkType(semanticModel, cref.Cref, frameworkType, context.CancellationToken))
+ continue;
+
+ var qualifiedTypeName = frameworkType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var diagnostic = Diagnostic.Create(
+ Rule,
+ cref.GetLocation(),
+ ImmutableDictionary.Empty.Add(QualifiedTypePropertyName, qualifiedTypeName),
+ cref.Cref.ToString(),
+ qualifiedTypeName.Substring("global::".Length)
+ );
+
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+
+ static bool IsAlreadyQualified(CrefSyntax cref) =>
+ cref switch
+ {
+ TypeCrefSyntax { Type: QualifiedNameSyntax or AliasQualifiedNameSyntax } => true,
+ NameMemberCrefSyntax { Name: QualifiedNameSyntax or AliasQualifiedNameSyntax } => true,
+ _ => false,
+ };
+
+ static bool ReferencesFrameworkType(
+ SemanticModel semanticModel,
+ CrefSyntax cref,
+ INamedTypeSymbol frameworkType,
+ CancellationToken cancellationToken
+ )
+ {
+ var symbolInfo = semanticModel.GetSymbolInfo(cref, cancellationToken);
+ if (SymbolMatchesFrameworkType(symbolInfo.Symbol, frameworkType))
+ return true;
+
+ foreach (var candidate in symbolInfo.CandidateSymbols)
+ {
+ if (SymbolMatchesFrameworkType(candidate, frameworkType))
+ return true;
+ }
+
+ return false;
+ }
+
+ static bool SymbolMatchesFrameworkType(ISymbol? symbol, INamedTypeSymbol frameworkType)
+ {
+ if (symbol is null)
+ return false;
+
+ var containingType = symbol switch
+ {
+ INamedTypeSymbol namedType => namedType,
+ IMethodSymbol method => method.ContainingType,
+ IPropertySymbol property => property.ContainingType,
+ IFieldSymbol field => field.ContainingType,
+ IEventSymbol @event => @event.ContainingType,
+ _ => null,
+ };
+
+ return containingType is not null
+ && SymbolEqualityComparer.Default.Equals(
+ containingType.OriginalDefinition,
+ frameworkType.OriginalDefinition
+ );
+ }
+
+ static bool TryGetUnqualifiedTypeName(CrefSyntax cref, out string typeName)
+ {
+ typeName = string.Empty;
+
+ if (cref is TypeCrefSyntax { Type: IdentifierNameSyntax identifierName })
+ {
+ typeName = identifierName.Identifier.ValueText;
+ return true;
+ }
+
+ if (cref is TypeCrefSyntax { Type: GenericNameSyntax genericName })
+ {
+ typeName = genericName.Identifier.ValueText;
+ return true;
+ }
+
+ if (cref is NameMemberCrefSyntax { Name: IdentifierNameSyntax identifierMemberName })
+ {
+ typeName = identifierMemberName.Identifier.ValueText;
+ return true;
+ }
+
+ if (cref is NameMemberCrefSyntax { Name: GenericNameSyntax genericMemberName })
+ {
+ typeName = genericMemberName.Identifier.ValueText;
+ return true;
+ }
+
+ return false;
+ }
+
+ static IAssemblySymbol? ResolveFrameworkAssembly(Compilation compilation) =>
+ compilation
+ .References.Select(compilation.GetAssemblyOrModuleSymbol)
+ .OfType()
+ .FirstOrDefault(static assembly =>
+ string.Equals(assembly.Identity.Name, FrameworkAssemblyName, StringComparison.Ordinal)
+ );
+
+ static ImmutableDictionary CollectPublicFrameworkTypes(INamespaceSymbol rootNamespace)
+ {
+ var builder = ImmutableDictionary.CreateBuilder(StringComparer.Ordinal);
+ CollectPublicFrameworkTypes(rootNamespace, builder);
+ return builder.ToImmutable();
+ }
+
+ static void CollectPublicFrameworkTypes(
+ INamespaceOrTypeSymbol container,
+ ImmutableDictionary.Builder builder
+ )
+ {
+ foreach (var member in container.GetMembers())
+ {
+ if (member is INamespaceSymbol namespaceSymbol)
+ {
+ CollectPublicFrameworkTypes(namespaceSymbol, builder);
+ continue;
+ }
+
+ if (member is not INamedTypeSymbol namedType)
+ continue;
+
+ if (RoslynComponentDiscovery.IsEffectivelyPublic(namedType) && !builder.ContainsKey(namedType.Name))
+ builder.Add(namedType.Name, namedType);
+
+ CollectPublicFrameworkTypes(namedType, builder);
+ }
+ }
+
+ static bool IsExplicitlyTrue(AnalyzerConfigOptions options, string propertyName) =>
+ options.TryGetValue("build_property." + propertyName, out var value)
+ && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
+}
diff --git a/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md b/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md
index 9ce7aaf..39dcd15 100644
--- a/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md
+++ b/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md
@@ -15,10 +15,11 @@ PSGFR36 | Purview.SourceGeneratorFramework | Warning | Extension class is not pl
PSGFR37 | Purview.SourceGeneratorFramework | Warning | Extension class extends multiple receiver types
PSGFR38 | Purview.SourceGeneratorFramework | Warning | Extension class is missing EditorBrowsable
PSGFR39 | Purview.SourceGeneratorFramework | Error | Roslyn component must produce a self-contained analyzer
+PSGFR40 | Purview.SourceGeneratorFramework | Warning | Unqualified SGF cref is ambiguous
TLB0014 | TypeLibrary | Warning | Type library partial extension is declared in a different namespace
TLB0015 | TypeLibrary | Info | Type library partial extension must be declared 'public static partial'
TLB0016 | TypeLibrary | Error | Enum value member type must be TypeIdentity or EnumValueDefinition
TLB0017 | TypeLibrary | Error | Enum value member references an enum type that is not declared
TLB0018 | TypeLibrary | Error | Duplicate enum value member
TLB0019 | TypeLibrary | Info | Duplicate enum value
-TLB0020 | TypeLibrary | Error | Enum values member references a type that is not an enum
\ No newline at end of file
+TLB0020 | TypeLibrary | Error | Enum values member references a type that is not an enum
diff --git a/src/src/SourceGeneratorFramework.CodeFixers/QualifyFrameworkCrefCodeFixProvider.cs b/src/src/SourceGeneratorFramework.CodeFixers/QualifyFrameworkCrefCodeFixProvider.cs
new file mode 100644
index 0000000..0fb4900
--- /dev/null
+++ b/src/src/SourceGeneratorFramework.CodeFixers/QualifyFrameworkCrefCodeFixProvider.cs
@@ -0,0 +1,77 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Purview.SourceGeneratorFramework.Analyzers;
+
+namespace Purview.SourceGeneratorFramework.CodeFixers;
+
+///
+/// Qualifies SGF XML documentation cref targets with their global:: name.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(QualifyFrameworkCrefCodeFixProvider))]
+public sealed class QualifyFrameworkCrefCodeFixProvider : CodeFixProvider
+{
+ internal const string EquivalenceKey = "QualifyFrameworkCref";
+
+ public override ImmutableArray FixableDiagnosticIds => [AmbiguousFrameworkCrefAnalyzer.DiagnosticId];
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null)
+ return;
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ if (
+ !diagnostic.Properties.TryGetValue(
+ AmbiguousFrameworkCrefAnalyzer.QualifiedTypePropertyName,
+ out var qualifiedTypeName
+ ) || string.IsNullOrWhiteSpace(qualifiedTypeName)
+ )
+ continue;
+
+ var resolvedQualifiedTypeName = qualifiedTypeName!;
+
+ var node = root.FindNode(
+ diagnostic.Location.SourceSpan,
+ getInnermostNodeForTie: true,
+ findInsideTrivia: true
+ );
+ var crefAttribute =
+ node as XmlCrefAttributeSyntax
+ ?? node.AncestorsAndSelf().OfType().FirstOrDefault();
+ if (crefAttribute is null)
+ continue;
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ "Qualify SGF cref with global::",
+ _ => QualifyCrefAsync(context.Document, root, crefAttribute, resolvedQualifiedTypeName),
+ EquivalenceKey
+ ),
+ diagnostic
+ );
+ }
+ }
+
+ static Task QualifyCrefAsync(
+ Document document,
+ SyntaxNode root,
+ XmlCrefAttributeSyntax crefAttribute,
+ string qualifiedTypeName
+ )
+ {
+ var replacement = crefAttribute
+ .WithCref(SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(qualifiedTypeName)))
+ .WithTriviaFrom(crefAttribute);
+
+ var newRoot = root.ReplaceNode(crefAttribute, replacement);
+ return Task.FromResult(document.WithSyntaxRoot(newRoot));
+ }
+}
diff --git a/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs b/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
index a8a8e34..3c0af06 100644
--- a/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
+++ b/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
@@ -3,6 +3,9 @@
static class MergeToolRunner
{
+ const string IsExternalInitName = "IsExternalInit";
+ const string IsExternalInitNamespace = "System.Runtime.CompilerServices";
+
public static int Run(string[] args, TextWriter error, ILogger? logger = null)
{
if (args.Length < 3)
@@ -64,7 +67,6 @@ public static int Run(string[] args, TextWriter error, ILogger? logger = null)
SearchDirectories = searchDirectories,
Internalize = true,
InternalizeAssemblies = [Path.GetFileNameWithoutExtension(frameworkPath)],
- RenameInternalized = false,
UnionMerge = true,
Parallel = true,
DebugInfo = File.Exists(
@@ -82,8 +84,8 @@ public static int Run(string[] args, TextWriter error, ILogger? logger = null)
new ILRepack(options, logger).Repack();
}
- var frameworkTypeNames = ReadTypeNames(frameworkPath, searchDirectories);
- InternalizeFrameworkTypes(outputPath, frameworkTypeNames, searchDirectories);
+ RestoreCanonicalIsExternalInit(outputPath, searchDirectories);
+
return 0;
}
finally
@@ -92,24 +94,27 @@ public static int Run(string[] args, TextWriter error, ILogger? logger = null)
}
}
- static HashSet ReadTypeNames(string assemblyPath, IEnumerable searchDirectories)
+ internal static IEnumerable Flatten(TypeDefinition type)
{
- using var resolver = CreateResolver(searchDirectories);
- using var assembly = AssemblyDefinition.ReadAssembly(
- assemblyPath,
- new ReaderParameters { AssemblyResolver = resolver }
- );
- return assembly
- .MainModule.Types.SelectMany(Flatten)
- .Select(static type => type.FullName)
- .ToHashSet(StringComparer.Ordinal);
+ yield return type;
+ foreach (var nestedType in type.NestedTypes.SelectMany(Flatten))
+ {
+ yield return nestedType;
+ }
}
- static void InternalizeFrameworkTypes(
- string assemblyPath,
- HashSet frameworkTypeNames,
- IEnumerable searchDirectories
- )
+ internal static DefaultAssemblyResolver CreateResolver(IEnumerable searchDirectories)
+ {
+ DefaultAssemblyResolver resolver = new();
+ foreach (var searchDirectory in searchDirectories)
+ {
+ resolver.AddSearchDirectory(searchDirectory);
+ }
+
+ return resolver;
+ }
+
+ static void RestoreCanonicalIsExternalInit(string assemblyPath, IEnumerable searchDirectories)
{
var pdbPath = Path.ChangeExtension(assemblyPath, ".pdb");
var hasSymbols = File.Exists(pdbPath);
@@ -124,38 +129,26 @@ IEnumerable searchDirectories
}
);
- foreach (var type in assembly.MainModule.Types.SelectMany(Flatten))
- {
- if (!frameworkTypeNames.Contains(type.FullName))
- {
- continue;
- }
+ var marker = assembly
+ .MainModule.Types.SelectMany(Flatten)
+ .SingleOrDefault(static type =>
+ type.Name == IsExternalInitName
+ || type.Name.EndsWith(IsExternalInitName, StringComparison.Ordinal)
+ || (
+ type.Namespace.Contains(IsExternalInitNamespace, StringComparison.Ordinal)
+ && type.Name.Contains(IsExternalInitName, StringComparison.Ordinal)
+ )
+ );
- type.Attributes = type.IsNested
- ? (type.Attributes & ~TypeAttributes.VisibilityMask) | TypeAttributes.NestedAssembly
- : (type.Attributes & ~TypeAttributes.VisibilityMask) | TypeAttributes.NotPublic;
- }
+ if (marker is null)
+ return;
- assembly.Write(assemblyPath, new WriterParameters { WriteSymbols = hasSymbols });
- }
+ marker.Namespace = IsExternalInitNamespace;
+ marker.Name = IsExternalInitName;
+ marker.Attributes = marker.IsNested
+ ? (marker.Attributes & ~TypeAttributes.VisibilityMask) | TypeAttributes.NestedAssembly
+ : (marker.Attributes & ~TypeAttributes.VisibilityMask) | TypeAttributes.NotPublic;
- internal static IEnumerable Flatten(TypeDefinition type)
- {
- yield return type;
- foreach (var nestedType in type.NestedTypes.SelectMany(Flatten))
- {
- yield return nestedType;
- }
- }
-
- internal static DefaultAssemblyResolver CreateResolver(IEnumerable searchDirectories)
- {
- DefaultAssemblyResolver resolver = new();
- foreach (var searchDirectory in searchDirectories)
- {
- resolver.AddSearchDirectory(searchDirectory);
- }
-
- return resolver;
+ assembly.Write(assemblyPath, new WriterParameters { WriteSymbols = hasSymbols });
}
}
diff --git a/src/src/SourceGeneratorFramework.Testing/RoslynTestRunner.cs b/src/src/SourceGeneratorFramework.Testing/RoslynTestRunner.cs
index 93e4778..2639df7 100644
--- a/src/src/SourceGeneratorFramework.Testing/RoslynTestRunner.cs
+++ b/src/src/SourceGeneratorFramework.Testing/RoslynTestRunner.cs
@@ -34,12 +34,53 @@ SourceGeneratorTestOptions options
);
}
+ if (options.CompilationWithAnalyzersOptions is null && options.AnalyzerOptions is null)
+ {
+ var analyzerConfigOptions = BuildAnalyzerConfig(options);
+ if (analyzerConfigOptions.Count > 0)
+ {
+ return compilation.WithAnalyzers(
+ analyzers,
+ new AnalyzerOptions(
+ ImmutableArray.Empty,
+ new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions)
+ )
+ );
+ }
+ }
+
// If the caller provided CompilationWithAnalyzersOptions, use that; otherwise, use AnalyzerOptions.
return options.CompilationWithAnalyzersOptions is not null
? compilation.WithAnalyzers(analyzers, options.CompilationWithAnalyzersOptions)
: compilation.WithAnalyzers(analyzers, options.AnalyzerOptions);
}
+ static Dictionary BuildAnalyzerConfig(SourceGeneratorTestOptions options)
+ {
+ Dictionary analyzerOptions = new(options.AnalyzerConfigOptions)
+ {
+ [SourceGeneratorBuildProperties.ValidateCodeWriterScopes] = options.ValidateCodeWriterScopes.ToString(),
+ [SourceGeneratorBuildProperties.EnableLogging] = options.EnableLogging.ToString(),
+ };
+
+ foreach (var pair in options.AnalyzerConfigOptions)
+ {
+ if (!pair.Key.StartsWith(SourceGeneratorBuildProperties.BuildProperty, StringComparison.Ordinal))
+ analyzerOptions[SourceGeneratorBuildProperties.BuildProperty + pair.Key] = pair.Value;
+ }
+
+ if (options.DisableSourceGeneratorPropertyName is not null && options.DisableSourceGeneratorValue is not null)
+ {
+ var disablePropertyName = options.DisableSourceGeneratorPropertyName;
+ if (!disablePropertyName.StartsWith(SourceGeneratorBuildProperties.BuildProperty, StringComparison.Ordinal))
+ disablePropertyName = SourceGeneratorBuildProperties.BuildProperty + disablePropertyName;
+
+ analyzerOptions[disablePropertyName] = options.DisableSourceGeneratorValue.Value.ToString();
+ }
+
+ return analyzerOptions;
+ }
+
///
/// Creates a project containing the supplied sources.
///
diff --git a/src/src/SourceGeneratorFramework.Testing/TestAnalyzerConfigOptionsProvider.cs b/src/src/SourceGeneratorFramework.Testing/TestAnalyzerConfigOptionsProvider.cs
index 07c8746..d6467e6 100644
--- a/src/src/SourceGeneratorFramework.Testing/TestAnalyzerConfigOptionsProvider.cs
+++ b/src/src/SourceGeneratorFramework.Testing/TestAnalyzerConfigOptionsProvider.cs
@@ -20,7 +20,7 @@ sealed class TestAnalyzerConfigOptionsProvider(Dictionary option
public override AnalyzerConfigOptions GlobalOptions => _globalOptions;
///
- public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => TestAnalyzerConfigOptions.Empty;
+ public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => _globalOptions;
///
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => TestAnalyzerConfigOptions.Empty;
diff --git a/src/src/SourceGeneratorFramework/Sdk/README.md b/src/src/SourceGeneratorFramework/Sdk/README.md
index f29faa3..3689549 100644
--- a/src/src/SourceGeneratorFramework/Sdk/README.md
+++ b/src/src/SourceGeneratorFramework/Sdk/README.md
@@ -989,6 +989,7 @@ The `Purview.SourceGeneratorFramework` package includes the `Purview.SourceGener
| `PSGFR36` | Extension classes must be placed in the extended type's namespace under an `Extensions` folder. |
| `PSGFR37` | One extension class per receiver type; split classes that extend multiple types. |
| `PSGFR38` | Extension classes should carry `[EditorBrowsable(EditorBrowsableState.Never)]`. |
+| `PSGFR40` | In Roslyn components (`IsRoslynComponent=true`), qualify XML doc `cref` references to SGF public types with `global::Purview.SourceGeneratorFramework...`. |
## Documentation
diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/AmbiguousFrameworkCrefAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/AmbiguousFrameworkCrefAnalyzerTests.cs
new file mode 100644
index 0000000..d29e36e
--- /dev/null
+++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/AmbiguousFrameworkCrefAnalyzerTests.cs
@@ -0,0 +1,165 @@
+using System.Collections.Immutable;
+using Purview.SourceGeneratorFramework.Testing;
+using Purview.SourceGeneratorFramework.Testing.TUnit;
+
+namespace Purview.SourceGeneratorFramework.Analyzers;
+
+public sealed class AmbiguousFrameworkCrefAnalyzerTests
+ : TUnitDiagnosticAnalyzerTestBase
+{
+ static readonly AnalyzerTestOptions RoslynComponentOptions = new()
+ {
+ AnalyzerConfigOptions = new Dictionary
+ {
+ ["build_property.IsRoslynComponent"] = "true",
+ }.ToImmutableDictionary(),
+ AdditionalAssemblyTypes =
+ [
+ typeof(Purview.SourceGeneratorFramework.CodeWriter),
+ typeof(Purview.SourceGeneratorFramework.GenerationSettings),
+ typeof(Purview.SourceGeneratorFramework.TypeIdentity),
+ typeof(Purview.SourceGeneratorFramework.TypeReference),
+ typeof(Purview.SourceGeneratorFramework.XmlCommentWriter),
+ typeof(Purview.SourceGeneratorFramework.Helpers.IncrementalPipeline),
+ typeof(Purview.SourceGeneratorFramework.CodeWriterScopeValidationException),
+ ],
+ };
+
+ [Test]
+ public async Task BareSgfCrefs_ReportDiagnosticsForMultipleTypes(CancellationToken cancellationToken)
+ {
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+ using Purview.SourceGeneratorFramework.Helpers;
+
+ ///
+ /// Shared building blocks.
+ /// Writes with .
+ /// Configures .
+ /// Tracks .
+ /// Emits docs with .
+ /// Composes providers with .
+ /// Throws when scope validation fails.
+ ///
+ public static class TypeRefs
+ {
+ }
+ """;
+
+ var result = await AnalyzeAsync(source, RoslynComponentOptions, cancellationToken);
+
+ await Assert.That(result).HasDiagnostics(7);
+ await Assert.That(result).HasDiagnostic(AmbiguousFrameworkCrefAnalyzer.DiagnosticId);
+ }
+
+ [Test]
+ public async Task QualifiedSgfCrefs_DoNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ ///
+ /// Shared building blocks.
+ ///
+ public static class TypeRefs
+ {
+ }
+ """;
+
+ var result = await AnalyzeAsync(source, RoslynComponentOptions, cancellationToken);
+
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task NonSgfType_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ const string source = """
+ using System;
+
+ ///
+ /// Shared building blocks.
+ ///
+ public static class TypeRefs
+ {
+ }
+ """;
+
+ var result = await AnalyzeAsync(source, RoslynComponentOptions, cancellationToken);
+
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task ShadowTypeWithSgfName_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ const string source = """
+ namespace Example;
+
+ public sealed class TypeReference
+ {
+ }
+
+ ///
+ /// Shared building blocks.
+ ///
+ public static class TypeRefs
+ {
+ }
+ """;
+
+ var result = await AnalyzeAsync(source, RoslynComponentOptions, cancellationToken);
+
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task MissingRoslynComponentOptIn_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ ///
+ /// Shared building blocks.
+ ///
+ public static class TypeRefs
+ {
+ }
+ """;
+
+ var result = await AnalyzeAsync(
+ source,
+ new AnalyzerTestOptions { AdditionalAssemblyTypes = RoslynComponentOptions.AdditionalAssemblyTypes },
+ cancellationToken
+ );
+
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task RoslynComponentOptOut_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ ///
+ /// Shared building blocks.
+ ///
+ public static class TypeRefs
+ {
+ }
+ """;
+
+ var options = RoslynComponentOptions with
+ {
+ AnalyzerConfigOptions = new Dictionary
+ {
+ ["build_property.IsRoslynComponent"] = "false",
+ }.ToImmutableDictionary(),
+ };
+
+ var result = await AnalyzeAsync(source, options, cancellationToken);
+
+ await Assert.That(result).HasNoDiagnostics();
+ }
+}
diff --git a/src/tests/SourceGeneratorFramework.CodeFixers.UnitTests/QualifyFrameworkCrefCodeFixProviderTests.cs b/src/tests/SourceGeneratorFramework.CodeFixers.UnitTests/QualifyFrameworkCrefCodeFixProviderTests.cs
new file mode 100644
index 0000000..d3fd9a8
--- /dev/null
+++ b/src/tests/SourceGeneratorFramework.CodeFixers.UnitTests/QualifyFrameworkCrefCodeFixProviderTests.cs
@@ -0,0 +1,93 @@
+using System.Collections.Immutable;
+using Purview.SourceGeneratorFramework.Analyzers;
+using Purview.SourceGeneratorFramework.Testing;
+using Purview.SourceGeneratorFramework.Testing.TUnit;
+
+namespace Purview.SourceGeneratorFramework.CodeFixers;
+
+public sealed class QualifyFrameworkCrefCodeFixProviderTests
+ : TUnitCodeFixTestBase
+{
+ static CodeFixTestOptions Options =>
+ new()
+ {
+ EquivalenceKey = QualifyFrameworkCrefCodeFixProvider.EquivalenceKey,
+ AnalyzerConfigOptions = new Dictionary
+ {
+ ["build_property.IsRoslynComponent"] = "true",
+ }.ToImmutableDictionary(),
+ AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(TypeReference)],
+ };
+
+ [Test]
+ public async Task BareTypeReferenceCref_IsQualified(CancellationToken cancellationToken)
+ {
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ ///
+ /// Shared building blocks.
+ ///
+ public static class TypeRefs
+ {
+ }
+ """;
+
+ var result = await ApplyCodeFixAsync(source, Options, cancellationToken);
+
+ await Assert.That(result).HasDiagnostic(AmbiguousFrameworkCrefAnalyzer.DiagnosticId);
+ await Assert
+ .That(result.FixedSource)
+ .Contains("");
+ }
+
+ [Test]
+ public async Task BareCodeWriterCref_IsQualified(CancellationToken cancellationToken)
+ {
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ ///
+ /// Writes with .
+ ///
+ public static class TypeRefs
+ {
+ }
+ """;
+
+ var result = await ApplyCodeFixAsync(source, Options, cancellationToken);
+
+ await Assert.That(result).HasDiagnostic(AmbiguousFrameworkCrefAnalyzer.DiagnosticId);
+ await Assert
+ .That(result.FixedSource)
+ .Contains("");
+ }
+
+ [Test]
+ public async Task FixAll_QualifiesMultipleCrefs(CancellationToken cancellationToken)
+ {
+ const string source = """
+ using Purview.SourceGeneratorFramework;
+
+ ///
+ /// Shared building blocks.
+ /// Writes with .
+ ///
+ public static class TypeRefs
+ {
+ }
+ """;
+
+ var result = await ApplyFixAllAsync(source, Options, cancellationToken);
+
+ await Assert
+ .That(result.Diagnostics.Select(static d => d.Id).ToArray())
+ .Contains(AmbiguousFrameworkCrefAnalyzer.DiagnosticId);
+ await Assert
+ .That(result.FixedSources["Test1.cs"])
+ .Contains("");
+ await Assert
+ .That(result.FixedSources["Test1.cs"])
+ .Contains("");
+ }
+}
diff --git a/src/tests/SourceGeneratorFramework.MergeTool.UnitTests/MergeToolRunnerTests.cs b/src/tests/SourceGeneratorFramework.MergeTool.UnitTests/MergeToolRunnerTests.cs
index 7394c32..07c2325 100644
--- a/src/tests/SourceGeneratorFramework.MergeTool.UnitTests/MergeToolRunnerTests.cs
+++ b/src/tests/SourceGeneratorFramework.MergeTool.UnitTests/MergeToolRunnerTests.cs
@@ -10,7 +10,6 @@ namespace Purview.SourceGeneratorFramework.MergeTool;
public sealed class MergeToolRunnerTests
{
const string MarkerFullName = "System.Runtime.CompilerServices.IsExternalInit";
-
const string FrameworkSource = """
namespace System.Runtime.CompilerServices
{
@@ -19,6 +18,13 @@ public static class IsExternalInit;
namespace Fixture.Framework
{
+ public sealed class TypeReference
+ {
+ public string Name { get; }
+
+ public TypeReference(string name) => Name = name;
+ }
+
public enum OptionKind
{
None,
@@ -45,6 +51,8 @@ namespace Fixture.Component
public static class Consumer
{
public static Options Create() => new() { Kind = OptionKind.Enabled };
+
+ public static string Describe() => new TypeReference("Merged").Name;
}
public sealed class ComponentOptions
@@ -62,6 +70,8 @@ namespace Fixture.Component
public static class Consumer
{
public static Options Create() => new() { Kind = OptionKind.Enabled };
+
+ public static string Describe() => new TypeReference("Merged").Name;
}
}
""";
@@ -150,6 +160,10 @@ await Assert
.That(merged.MainModule.AssemblyReferences.Any(static reference => reference.Name == "Fixture.Framework"))
.IsFalse();
+ var mergedTypeReference = merged.MainModule.GetType("Fixture.Framework.TypeReference");
+ await Assert.That(mergedTypeReference).IsNotNull();
+ await Assert.That(mergedTypeReference!.IsNotPublic).IsTrue();
+
var consumer = merged.MainModule.GetType("Fixture.Component.Consumer");
var create = consumer.Methods.Single(static method => method.Name == "Create");
var setter = create
@@ -183,6 +197,11 @@ static async Task AssertMergedAssemblyExecutesAsync(string outputPath)
.Invoke(null, null);
var kind = created!.GetType().GetProperty("Kind")!.GetValue(created);
await Assert.That(kind!.ToString()).IsEqualTo("Enabled");
+ var described = loaded
+ .GetType("Fixture.Component.Consumer", throwOnError: true)!
+ .GetMethod("Describe", BindingFlags.Public | BindingFlags.Static)!
+ .Invoke(null, null);
+ await Assert.That(described).IsEqualTo("Merged");
}
finally
{
@@ -274,23 +293,29 @@ public void Dispose()
static string GetNetStandardReferenceDirectory()
{
- var packageRoot = Environment.GetEnvironmentVariable("NUGET_PACKAGES");
- if (string.IsNullOrWhiteSpace(packageRoot))
+ var candidateRoots = new[]
{
- packageRoot = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
- ".nuget",
- "packages"
- );
+ Environment.GetEnvironmentVariable("NUGET_PACKAGES"),
+ Environment.GetEnvironmentVariable("RestorePackagesPath"),
+ Environment.GetEnvironmentVariable("NuGetPackageRoot"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"),
}
+ .Where(static path => !string.IsNullOrWhiteSpace(path))
+ .Select(static path => path!)
+ .Select(static path => path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
+ .Distinct(StringComparer.OrdinalIgnoreCase);
- var path = Path.Combine(packageRoot, "netstandard.library", "2.0.3", "build", "netstandard2.0", "ref");
+ foreach (var packageRoot in candidateRoots)
+ {
+ var path = Path.Combine(packageRoot, "netstandard.library", "2.0.3", "build", "netstandard2.0", "ref");
- return Directory.Exists(path)
- ? path
- : throw new DirectoryNotFoundException(
- $"The .NET Standard 2.0 reference directory was not found: {path}"
- );
+ if (Directory.Exists(path))
+ return path;
+ }
+
+ throw new DirectoryNotFoundException(
+ $"The .NET Standard 2.0 reference directory was not found under any known package roots: {string.Join(", ", candidateRoots)}"
+ );
}
}
}