Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/wiki/Analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -113,4 +115,4 @@ reload the project for the fixes to appear.

## License

This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project.
This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
[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<DiagnosticDescriptor> 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<string, INamedTypeSymbol> 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<XmlCrefAttributeSyntax>() ?? [])
{
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<string, string?>.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<IAssemblySymbol>()
.FirstOrDefault(static assembly =>
string.Equals(assembly.Identity.Name, FrameworkAssemblyName, StringComparison.Ordinal)
);

static ImmutableDictionary<string, INamedTypeSymbol> CollectPublicFrameworkTypes(INamespaceSymbol rootNamespace)
{
var builder = ImmutableDictionary.CreateBuilder<string, INamedTypeSymbol>(StringComparer.Ordinal);
CollectPublicFrameworkTypes(rootNamespace, builder);
return builder.ToImmutable();
}

static void CollectPublicFrameworkTypes(
INamespaceOrTypeSymbol container,
ImmutableDictionary<string, INamedTypeSymbol>.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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
TLB0020 | TypeLibrary | Error | Enum values member references a type that is not an enum
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Qualifies SGF XML documentation cref targets with their <c>global::</c> name.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(QualifyFrameworkCrefCodeFixProvider))]
public sealed class QualifyFrameworkCrefCodeFixProvider : CodeFixProvider
{
internal const string EquivalenceKey = "QualifyFrameworkCref";

public override ImmutableArray<string> 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<XmlCrefAttributeSyntax>().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<Document> 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));
}
}
Loading
Loading