Skip to content

Commit 24dc131

Browse files
Add warning handling during interface generation and extend localized resources
1 parent e2b18b1 commit 24dc131

27 files changed

Lines changed: 2494 additions & 1895 deletions

DIAGNOSTICS.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,27 @@ example scenarios. IDs and anchors are stable; message text is localized.
376376
- See: [dependent-compositions](readme/dependent-compositions.md).
377377
- Examples: Binding in a dependent setup uses instance field/property.
378378

379+
### DIW008
380+
- Description: GenerateInterface is applied to a non-public element.
381+
- Problem: `[GenerateInterface]` was placed on a non-public method/property/event.
382+
- Fix: Make the element `public` or remove the attribute from that element.
383+
- See: [generate-interface](readme/generate-interface.md).
384+
- Examples: `[GenerateInterface]` on `private` or `internal` property.
385+
386+
### DIW009
387+
- Description: GenerateInterface is applied to a static element.
388+
- Problem: `[GenerateInterface]` was placed on a `static` method/property/event.
389+
- Fix: Move the contract element to an instance member or remove the attribute.
390+
- See: [generate-interface](readme/generate-interface.md).
391+
- Examples: `[GenerateInterface]` on `public static` method.
392+
393+
### DIW010
394+
- Description: Selective interface generation produced an empty interface.
395+
- Problem: Selective mode was enabled for an interface, but no eligible elements remained after filtering.
396+
- Fix: Mark at least one `public` instance element for that interface, or remove selective member annotations.
397+
- See: [generate-interface](readme/generate-interface.md).
398+
- Examples: All selected elements are `IgnoreInterface`, non-public, or static.
399+
379400
## Info
380401

381402
### DII000

src/Pure.DI.Core/Core/LogId.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ static class LogId
111111
public const string WarningTypeArgInResolveMethod = "DIW006";
112112
// DependsOn uses an instance member.
113113
public const string WarningInstanceMemberInDependsOnSetup = "DIW007";
114+
// GenerateInterface attribute is used on a non-public element.
115+
public const string WarningGenerateInterfaceOnNonPublicMember = "DIW008";
116+
// GenerateInterface attribute is used on a static element.
117+
public const string WarningGenerateInterfaceOnStaticMember = "DIW009";
118+
// Selective interface generation produced an empty interface.
119+
public const string WarningGenerateInterfaceSelectiveEmpty = "DIW010";
114120

115121
// Info
116122
// Generation was interrupted.

src/Pure.DI.Core/Core/LogMetadata.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ static class LogMetadata
6161
LogId.WarningRootArgInResolveMethod => $"{HelpLinkBaseUrl}#diw005",
6262
LogId.WarningTypeArgInResolveMethod => $"{HelpLinkBaseUrl}#diw006",
6363
LogId.WarningInstanceMemberInDependsOnSetup => $"{HelpLinkBaseUrl}#diw007",
64+
LogId.WarningGenerateInterfaceOnNonPublicMember => $"{HelpLinkBaseUrl}#diw008",
65+
LogId.WarningGenerateInterfaceOnStaticMember => $"{HelpLinkBaseUrl}#diw009",
66+
LogId.WarningGenerateInterfaceSelectiveEmpty => $"{HelpLinkBaseUrl}#diw010",
6467
LogId.InfoGenerationInterrupted => $"{HelpLinkBaseUrl}#dii000",
6568
LogId.InfoNotImplementedContract => $"{HelpLinkBaseUrl}#dii001",
6669
_ => null
@@ -125,6 +128,9 @@ public static string GetCategory(string id) =>
125128
LogId.ErrorInvalidAttributeArgumentPosition => "Validation",
126129
LogId.ErrorAttributeMemberCannotBeProcessed => "Validation",
127130
LogId.WarningInjectionSiteNotUsed => "Validation",
131+
LogId.WarningGenerateInterfaceOnNonPublicMember => "Validation",
132+
LogId.WarningGenerateInterfaceOnStaticMember => "Validation",
133+
LogId.WarningGenerateInterfaceSelectiveEmpty => "Validation",
128134

129135
// ReSharper disable once RedundantSwitchExpressionArms
130136
LogId.InfoGenerationInterrupted => "General",
@@ -188,6 +194,9 @@ public static string GetCategory(string id) =>
188194
LogId.WarningRootArgInResolveMethod => Strings.Description_WarningRootArgInResolveMethod,
189195
LogId.WarningTypeArgInResolveMethod => Strings.Description_WarningTypeArgInResolveMethod,
190196
LogId.WarningInstanceMemberInDependsOnSetup => Strings.Description_WarningInstanceMemberInDependsOnSetup,
197+
LogId.WarningGenerateInterfaceOnNonPublicMember => Strings.Description_WarningGenerateInterfaceOnNonPublicMember,
198+
LogId.WarningGenerateInterfaceOnStaticMember => Strings.Description_WarningGenerateInterfaceOnStaticMember,
199+
LogId.WarningGenerateInterfaceSelectiveEmpty => Strings.Description_WarningGenerateInterfaceSelectiveEmpty,
191200
LogId.InfoGenerationInterrupted => Strings.Description_InfoGenerationInterrupted,
192201
LogId.InfoNotImplementedContract => Strings.Description_InfoNotImplementedContract,
193202
LogId.ErrorUnhandled => Strings.Description_ErrorUnhandled,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Pure.DI.InterfaceGeneration;
2+
3+
sealed class GeneratedInterfaceWarning(
4+
string id,
5+
string message,
6+
string messageKey,
7+
Location location)
8+
{
9+
public string Id { get; } = id;
10+
11+
public string Message { get; } = message;
12+
13+
public string MessageKey { get; } = messageKey;
14+
15+
public Location Location { get; } = location;
16+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Pure.DI.InterfaceGeneration;
2+
3+
sealed class GeneratedInterfacesResult(
4+
ImmutableArray<GeneratedInterfaceSource> sources,
5+
ImmutableArray<GeneratedInterfaceWarning> warnings)
6+
{
7+
public ImmutableArray<GeneratedInterfaceSource> Sources { get; } = sources;
8+
9+
public ImmutableArray<GeneratedInterfaceWarning> Warnings { get; } = warnings;
10+
}

src/Pure.DI.Core/InterfaceGeneration/IInterfaceBuilder.cs

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

33
interface IInterfaceBuilder
44
{
5-
ImmutableArray<GeneratedInterfaceSource> BuildInterfacesFor(
5+
GeneratedInterfacesResult BuildInterfacesFor(
66
SemanticModel semanticModel,
77
ITypeSymbol typeSymbol,
88
ClassDeclarationSyntax classSyntax);

src/Pure.DI.Core/InterfaceGeneration/InterfaceBuilder.cs

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ sealed class InterfaceBuilder(
1414
IRoslynSymbols roslynSymbols,
1515
ITypes types,
1616
IArguments arguments,
17+
ILocationProvider locationProvider,
1718
Func<IBuilder<GeneratedInterfaceDetails, Lines>> interfaceCodeBuilderFactory)
1819
: IInterfaceBuilder
1920
{
@@ -35,14 +36,16 @@ sealed class InterfaceBuilder(
3536
globalNamespaceStyle: FullyQualifiedDisplayFormat.GlobalNamespaceStyle,
3637
miscellaneousOptions: FullyQualifiedDisplayFormat.MiscellaneousOptions);
3738

38-
public ImmutableArray<GeneratedInterfaceSource> BuildInterfacesFor(
39+
public GeneratedInterfacesResult BuildInterfacesFor(
3940
SemanticModel semanticModel,
4041
ITypeSymbol typeSymbol,
4142
ClassDeclarationSyntax classSyntax)
4243
{
4344
if (typeSymbol is not INamedTypeSymbol namedTypeSymbol)
4445
{
45-
return ImmutableArray<GeneratedInterfaceSource>.Empty;
46+
return new GeneratedInterfacesResult(
47+
ImmutableArray<GeneratedInterfaceSource>.Empty,
48+
ImmutableArray<GeneratedInterfaceWarning>.Empty);
4649
}
4750

4851
var generateInterfaceAttributeFullName = $"{Names.GlobalNamespacePrefix}{Names.GenerateInterfaceAttributeFullName}";
@@ -53,6 +56,8 @@ public ImmutableArray<GeneratedInterfaceSource> BuildInterfacesFor(
5356
var settingsByInterface = new Dictionary<InterfaceKey, (bool AsInternal, bool HasClassAttribute)>();
5457
var selectiveInterfaces = new HashSet<InterfaceKey>();
5558
var selectedMembersByInterface = new Dictionary<InterfaceKey, HashSet<ISymbol>>();
59+
var selectiveInterfaceLocations = new Dictionary<InterfaceKey, Location>();
60+
var warnings = new List<GeneratedInterfaceWarning>();
5661

5762
var classGenerateAttributes = typeSymbol.GetAttributes()
5863
.Where(x => x.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == generateInterfaceAttributeFullName)
@@ -76,12 +81,15 @@ public ImmutableArray<GeneratedInterfaceSource> BuildInterfacesFor(
7681
settingsByInterface[key] = (existing.AsInternal, true);
7782
}
7883

79-
var members = roslynSymbols.GetAllMembers(typeSymbol)
84+
var allMembers = roslynSymbols.GetAllMembers(typeSymbol)
85+
.Where(x => x.Kind is SymbolKind.Method or SymbolKind.Property or SymbolKind.Event)
86+
.ToList();
87+
var eligibleMembers = allMembers
8088
.Where(x => x.DeclaredAccessibility == Accessibility.Public)
8189
.Where(x => !x.IsStatic)
8290
.ToList();
8391

84-
foreach (var member in members)
92+
foreach (var member in allMembers)
8593
{
8694
var generationAttributes = member.GetAttributes()
8795
.Where(x => x.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == generateInterfaceAttributeFullName)
@@ -101,12 +109,38 @@ public ImmutableArray<GeneratedInterfaceSource> BuildInterfacesFor(
101109
defaultAsInternal);
102110
var key = new InterfaceKey(settings.namespaceName, settings.interfaceName);
103111
selectiveInterfaces.Add(key);
112+
if (!selectiveInterfaceLocations.ContainsKey(key))
113+
{
114+
selectiveInterfaceLocations.Add(key, GetLocation(member, generationAttribute));
115+
}
104116

105117
if (!settingsByInterface.TryGetValue(key, out var existing))
106118
{
107119
settingsByInterface[key] = (settings.asInternal, false);
108120
}
109121

122+
var memberName = member.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
123+
var interfaceDisplayName = GetInterfaceDisplayName(key);
124+
if (member.IsStatic)
125+
{
126+
warnings.Add(new GeneratedInterfaceWarning(
127+
LogId.WarningGenerateInterfaceOnStaticMember,
128+
string.Format(Strings.Warning_Template_GenerateInterfaceOnStaticMember, memberName, interfaceDisplayName),
129+
nameof(Strings.Warning_Template_GenerateInterfaceOnStaticMember),
130+
GetLocation(member, generationAttribute)));
131+
continue;
132+
}
133+
134+
if (member.DeclaredAccessibility != Accessibility.Public)
135+
{
136+
warnings.Add(new GeneratedInterfaceWarning(
137+
LogId.WarningGenerateInterfaceOnNonPublicMember,
138+
string.Format(Strings.Warning_Template_GenerateInterfaceOnNonPublicMember, memberName, interfaceDisplayName),
139+
nameof(Strings.Warning_Template_GenerateInterfaceOnNonPublicMember),
140+
GetLocation(member, generationAttribute)));
141+
continue;
142+
}
143+
110144
if (HasIgnoreAttribute(member))
111145
{
112146
continue;
@@ -124,7 +158,9 @@ public ImmutableArray<GeneratedInterfaceSource> BuildInterfacesFor(
124158

125159
if (settingsByInterface.Count == 0)
126160
{
127-
return ImmutableArray<GeneratedInterfaceSource>.Empty;
161+
return new GeneratedInterfacesResult(
162+
ImmutableArray<GeneratedInterfaceSource>.Empty,
163+
warnings.ToImmutableArray());
128164
}
129165

130166
var nullableContextEnabled = semanticModel.Compilation.Options.NullableContextOptions != NullableContextOptions.Disable;
@@ -135,11 +171,21 @@ public ImmutableArray<GeneratedInterfaceSource> BuildInterfacesFor(
135171
var settings = pair.Value;
136172
var interfaceMembers = selectiveInterfaces.Contains(key)
137173
? selectedMembersByInterface.TryGetValue(key, out var selectedMembers)
138-
? members.Where(selectedMembers.Contains).ToList()
174+
? eligibleMembers.Where(selectedMembers.Contains).ToList()
139175
: []
140176
: settings.HasClassAttribute
141-
? members.Where(x => !HasIgnoreAttribute(x)).ToList()
177+
? eligibleMembers.Where(x => !HasIgnoreAttribute(x)).ToList()
142178
: [];
179+
if (selectiveInterfaces.Contains(key)
180+
&& interfaceMembers.Count == 0
181+
&& selectiveInterfaceLocations.TryGetValue(key, out var selectiveLocation))
182+
{
183+
warnings.Add(new GeneratedInterfaceWarning(
184+
LogId.WarningGenerateInterfaceSelectiveEmpty,
185+
string.Format(Strings.Warning_Template_GenerateInterfaceSelectiveEmpty, GetInterfaceDisplayName(key)),
186+
nameof(Strings.Warning_Template_GenerateInterfaceSelectiveEmpty),
187+
selectiveLocation));
188+
}
143189

144190
var symbolDetails = new GeneratedInterfaceDetails(semanticModel, key.NamespaceName, key.InterfaceName, settings.AsInternal)
145191
{
@@ -156,7 +202,7 @@ public ImmutableArray<GeneratedInterfaceSource> BuildInterfacesFor(
156202
interfaceCodeBuilderFactory().Build(symbolDetails)));
157203
}
158204

159-
return generatedSources.ToImmutable();
205+
return new GeneratedInterfacesResult(generatedSources.ToImmutable(), warnings.ToImmutableArray());
160206
}
161207

162208
private ImmutableArray<MethodInfo> GetMethods(SemanticModel semanticModel, List<ISymbol> members, bool nullableContextEnabled)
@@ -403,4 +449,19 @@ private static T GetArgValue<T>(SemanticModel semanticModel, IReadOnlyList<Attri
403449
return value is { HasValue: true, Value: T typedValue } ? typedValue : defaultValue;
404450
}
405451

452+
private Location GetLocation(ISymbol member, AttributeData attribute)
453+
{
454+
if (attribute.ApplicationSyntaxReference?.GetSyntax() is AttributeSyntax attributeSyntax)
455+
{
456+
return locationProvider.GetLocation(attributeSyntax);
457+
}
458+
459+
return member.Locations.FirstOrDefault() ?? Location.None;
460+
}
461+
462+
private static string GetInterfaceDisplayName(InterfaceKey key) =>
463+
string.IsNullOrWhiteSpace(key.NamespaceName)
464+
? key.InterfaceName
465+
: $"{key.NamespaceName}.{key.InterfaceName}";
466+
406467
}

src/Pure.DI.Core/InterfaceGeneration/InterfaceGenerator.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Pure.DI.InterfaceGeneration;
77
using Microsoft.CodeAnalysis.Text;
88
using Pure.DI;
99
using CoreNames = Pure.DI.Core.Names;
10+
using static Pure.DI.Core.LogMetadata;
1011

1112
sealed class InterfaceGenerator(IInterfaceBuilder interfaceBuilder) : IInterfaceGenerator
1213
{
@@ -37,7 +38,13 @@ public void Generate(SourceProductionContext context, ImmutableArray<GeneratorSy
3738
continue;
3839
}
3940

40-
var generatedInterfaces = interfaceBuilder.BuildInterfacesFor(syntaxContext.SemanticModel, typeSymbol, classSyntax);
41+
var generatedInterfacesResult = interfaceBuilder.BuildInterfacesFor(syntaxContext.SemanticModel, typeSymbol, classSyntax);
42+
foreach (var warning in generatedInterfacesResult.Warnings)
43+
{
44+
ReportWarning(context, warning);
45+
}
46+
47+
var generatedInterfaces = generatedInterfacesResult.Sources;
4148
if (generatedInterfaces.IsDefaultOrEmpty)
4249
{
4350
continue;
@@ -101,4 +108,19 @@ private static string SanitizeFileNamePart(string value) => string.IsNullOrWhite
101108
.Replace(',', '_')
102109
.Replace(' ', '_')
103110
.Replace(':', '_');
111+
112+
private static void ReportWarning(SourceProductionContext context, GeneratedInterfaceWarning warning)
113+
{
114+
var descriptor = new DiagnosticDescriptor(
115+
warning.Id,
116+
"WRN",
117+
warning.Message,
118+
GetCategory(warning.Id),
119+
DiagnosticSeverity.Warning,
120+
isEnabledByDefault: true,
121+
description: GetDescription(warning.Id),
122+
helpLinkUri: GetHelpLink(warning.Id));
123+
var properties = ImmutableDictionary<string, string?>.Empty.Add("puredi.messageKey", warning.MessageKey);
124+
context.ReportDiagnostic(Diagnostic.Create(descriptor, warning.Location, properties));
125+
}
104126
}

src/Pure.DI.Core/Strings.Designer.cs

Lines changed: 54 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)