@@ -37,132 +37,124 @@ public class DSPA0001_PublicMemberShouldBeAnnotatedWithPublicAPIAnalyzer : Diagn
3737
3838 public override void Initialize ( AnalysisContext context )
3939 {
40- // You must call this method to avoid analyzing generated code.
4140 context . ConfigureGeneratedCodeAnalysis ( GeneratedCodeAnalysisFlags . None ) ;
42-
43- // You must call this method to enable the Concurrent Execution.
4441 context . EnableConcurrentExecution ( ) ;
4542
46- // Register for symbol actions
47- context . RegisterSymbolAction ( AnalyzeSymbol ,
48- SymbolKind . NamedType ,
49- SymbolKind . Method ,
50- SymbolKind . Property ,
51- SymbolKind . Field ,
52- SymbolKind . Event ) ;
43+ context . RegisterCompilationStartAction ( compilationContext =>
44+ {
45+ var configCache = new ConcurrentDictionary < string , ImmutableHashSet < string > > ( StringComparer . Ordinal ) ;
46+ compilationContext . RegisterSymbolAction ( symbolContext => AnalyzeSymbol ( symbolContext , configCache ) ,
47+ SymbolKind . NamedType ,
48+ SymbolKind . Method ,
49+ SymbolKind . Property ,
50+ SymbolKind . Field ,
51+ SymbolKind . Event ) ;
52+ } ) ;
5353 }
5454
55- private static void AnalyzeSymbol ( SymbolAnalysisContext context )
55+ private static void AnalyzeSymbol ( SymbolAnalysisContext context , ConcurrentDictionary < string , ImmutableHashSet < string > > configCache )
5656 {
5757 var symbol = context . Symbol ;
5858
59- // Only check public symbols
60- if ( symbol . DeclaredAccessibility != Accessibility . Public )
61- return ;
62-
63- // If public, check if containing type is also public
64- if ( symbol . ContainingType ? . DeclaredAccessibility is not Accessibility . Public and not null )
59+ if ( symbol . IsImplicitlyDeclared )
6560 return ;
6661
67- var optionsProvider = context . Options . AnalyzerConfigOptionsProvider ;
68-
69- optionsProvider . GlobalOptions . TryGetValue ( "dotnet_code_quality.DecSm_Analyzers_ValidPublicApiAttributes" ,
70- out var values ) ;
62+ if ( symbol is IMethodSymbol methodSymbol )
63+ {
64+ if ( methodSymbol . MethodKind is MethodKind . Constructor or MethodKind . StaticConstructor or
65+ MethodKind . PropertyGet or MethodKind . PropertySet or
66+ MethodKind . EventAdd or MethodKind . EventRemove or MethodKind . EventRaise )
67+ return ;
7168
72- var options = GeneratorOptions . Parse ( values ) ;
69+ if ( methodSymbol . IsOverride )
70+ return ;
71+ }
7372
74- var tree = symbol . Locations . FirstOrDefault ( )
75- ? . SourceTree ;
73+ if ( ! IsEffectivelyPublic ( symbol ) )
74+ return ;
7675
77- if ( tree != null &&
78- optionsProvider
79- . GetOptions ( tree )
80- . TryGetValue ( "dotnet_code_quality.DecSm_Analyzers_ValidPublicApiAttributes" , out var fileValues ) )
81- options = GeneratorOptions . Parse ( fileValues ) ;
76+ var validAttributes = GetValidAttributes ( context , configCache ) ;
8277
83- var validAttributes = options . ValidAttributes . ToImmutableHashSet ( ) ;
78+ if ( HasValidAttributeRecursive ( symbol , validAttributes ) )
79+ return ;
8480
85- // Skip if the symbol itself has [PublicAPI]
86- if ( HasValidAttribute ( symbol , validAttributes ) )
81+ if ( symbol . Name == "" || validAttributes . Contains ( symbol . Name ) )
8782 return ;
8883
89- // Skip if the containing type has [PublicAPI]
90- var containingType = symbol . ContainingType ;
84+ context . ReportDiagnostic ( Diagnostic . Create ( Rule , symbol . Locations [ 0 ] , symbol . Name ) ) ;
85+ }
9186
92- while ( containingType != null )
87+ private static bool IsEffectivelyPublic ( ISymbol symbol )
88+ {
89+ var current = symbol ;
90+ while ( current != null )
9391 {
94- if ( HasValidAttribute ( containingType , validAttributes ) )
95- return ;
92+ if ( current . DeclaredAccessibility != Accessibility . Public )
93+ return false ;
9694
97- containingType = containingType . ContainingType ;
95+ current = current . ContainingType ;
9896 }
9997
100- // Special case: Implicitly declared symbols (like default constructors)
101- if ( symbol . IsImplicitlyDeclared )
102- return ;
103-
104- // Special case: .ctor methods
105- if ( symbol is { Kind : SymbolKind . Method , Name : ".ctor" } )
106- return ;
98+ return true ;
99+ }
107100
108- // Special case: Nameless symbols
109- // Also don't flag the Public API attributes themselves
110- if ( symbol . Name == "" ||
111- IsValidAttributeName ( symbol . Name , validAttributes ) ||
112- symbol . Name == "PublicAPI" ||
113- symbol . Name == "PublicAPIAttribute" )
114- return ;
101+ private static ImmutableHashSet < string > GetValidAttributes ( SymbolAnalysisContext context , ConcurrentDictionary < string , ImmutableHashSet < string > > cache )
102+ {
103+ var optionsProvider = context . Options . AnalyzerConfigOptionsProvider ;
104+ var tree = context . Symbol . Locations . Length > 0 ? context . Symbol . Locations [ 0 ] . SourceTree : null ;
115105
116- if ( symbol is IMethodSymbol methodSymbol )
106+ if ( tree != null && optionsProvider . GetOptions ( tree ) . TryGetValue ( "dotnet_code_quality.DecSm_Analyzers_ValidPublicApiAttributes" , out var treeValues ) )
117107 {
118- // Skip property/event accessors, as we check the property/event itself
119- if ( methodSymbol . MethodKind is MethodKind . PropertyGet
120- or MethodKind . PropertySet
121- or MethodKind . EventAdd
122- or MethodKind . EventRemove
123- or MethodKind . EventRaise )
124- return ;
125-
126- if ( methodSymbol . IsOverride )
127- return ;
108+ return ParseAndCache ( treeValues , cache ) ;
128109 }
129110
130- context . ReportDiagnostic ( Diagnostic . Create ( Rule , symbol . Locations [ 0 ] , symbol . Name ) ) ;
111+ optionsProvider . GlobalOptions . TryGetValue ( "dotnet_code_quality.DecSm_Analyzers_ValidPublicApiAttributes" , out var globalValues ) ;
112+ return ParseAndCache ( globalValues ?? string . Empty , cache ) ;
131113 }
132114
133- private static bool IsValidAttributeName ( string name , ImmutableHashSet < string > attributeNames )
115+ private static ImmutableHashSet < string > ParseAndCache ( string values , ConcurrentDictionary < string , ImmutableHashSet < string > > cache )
134116 {
135- if ( attributeNames . Contains ( name ) )
136- return true ;
137-
138- return name . EndsWith ( "Attribute" , StringComparison . Ordinal ) &&
139- name . Length > 9 &&
140- attributeNames . Contains ( name . Substring ( 0 , name . Length - 9 ) ) ;
117+ return cache . GetOrAdd ( values , v =>
118+ {
119+ var builder = ImmutableHashSet . CreateBuilder < string > ( StringComparer . Ordinal ) ;
120+ builder . Add ( "PublicAPI" ) ;
121+ builder . Add ( "PublicAPIAttribute" ) ;
122+
123+ if ( ! string . IsNullOrWhiteSpace ( v ) )
124+ {
125+ var parts = v . Split ( new [ ] { ',' } , StringSplitOptions . RemoveEmptyEntries ) ;
126+ foreach ( var part in parts )
127+ {
128+ var trimmed = part . Trim ( ) ;
129+ if ( string . IsNullOrWhiteSpace ( trimmed ) )
130+ continue ;
131+
132+ builder . Add ( trimmed ) ;
133+ if ( ! trimmed . EndsWith ( "Attribute" , StringComparison . Ordinal ) )
134+ {
135+ builder . Add ( trimmed + "Attribute" ) ;
136+ }
137+ }
138+ }
139+
140+ return builder . ToImmutable ( ) ;
141+ } ) ;
141142 }
142143
143- private static bool HasValidAttribute ( ISymbol symbol , ImmutableHashSet < string > attributeNames ) =>
144- symbol
145- . GetAttributes ( )
146- . Any ( attr => attr . AttributeClass != null && IsValidAttributeName ( attr . AttributeClass . Name , attributeNames ) ) ;
147- }
144+ private static bool HasValidAttributeRecursive ( ISymbol symbol , ImmutableHashSet < string > validAttributes )
145+ {
146+ var current = symbol ;
147+ while ( current != null )
148+ {
149+ foreach ( var attribute in current . GetAttributes ( ) )
150+ {
151+ if ( attribute . AttributeClass != null && validAttributes . Contains ( attribute . AttributeClass . Name ) )
152+ return true ;
153+ }
148154
149- public sealed record GeneratorOptions ( string [ ] ValidAttributes )
150- {
151- public string [ ] ValidAttributes { get ; } = ValidAttributes ;
155+ current = current . ContainingType ;
156+ }
152157
153- public static GeneratorOptions Parse ( string ? s )
154- {
155- if ( string . IsNullOrWhiteSpace ( s ) )
156- return new ( [ "PublicAPI" ] ) ;
157-
158- var results = s !
159- . Split ( ',' )
160- . Select ( x => x . Trim ( ) )
161- . Where ( x => ! string . IsNullOrWhiteSpace ( x ) )
162- . Append ( "PublicAPI" )
163- . Distinct ( )
164- . ToArray ( ) ;
165-
166- return new ( results ) ;
158+ return false ;
167159 }
168160}
0 commit comments