33//
44// This source code is licensed under the MIT license found in the
55// LICENSE file in the root directory of this source tree.
6+ using BB84 . SourceGenerators . Analyzers ;
67using BB84 . SourceGenerators . Attributes ;
78using BB84 . SourceGenerators . Extensions ;
89using BB84 . SourceGenerators . Helpers ;
@@ -45,6 +46,9 @@ private void Execute(SourceProductionContext context, (ClassDeclarationSyntax Cl
4546
4647 List < SectionInfo > sections = GetSections ( ctx . ClassDeclaration , ctx . SemanticModel , sectionDelimiter , serializeComments ) ;
4748
49+ if ( ! ValidateSectionProperties ( context , ctx . ClassDeclaration , ctx . SemanticModel ) )
50+ return ;
51+
4852 SourceBuilder sb = new ( ) ;
4953
5054 sb . AppendAutoGeneratedWarning ( GeneratorNames . IniFileGeneratorFullName ) ;
@@ -127,33 +131,6 @@ private static void AppendReadMethod(SourceBuilder sb, string className, string
127131 sb . AppendLine ( $ "{ elsePrefix } if (string.Equals(currentSection, \" { section . SectionName } \" , StringComparison.{ stringComparison } ))") ;
128132 sb . OpenBrace ( ) ;
129133
130- if ( section . NeedsInitialization )
131- {
132- sb . AppendLine ( $ "if (result.{ section . PropertyPath } == null)") ;
133- sb . Indent ( ) ;
134- sb . AppendLine ( $ "result.{ section . PropertyPath } = new { section . TypeName } ();") ;
135- sb . Outdent ( ) ;
136- sb . AppendLine ( ) ;
137- }
138- else if ( section . PropertyPath . Contains ( "." ) )
139- {
140- // Ensure parent path segments are initialized for nested sections
141- string [ ] segments = section . PropertyPath . Split ( '.' ) ;
142- for ( int i = 0 ; i < segments . Length - 1 ; i ++ )
143- {
144- string parentPath = string . Join ( "." , segments , 0 , i + 1 ) ;
145- SectionInfo ? parentSection = sections . FirstOrDefault ( ps => ps . PropertyPath == parentPath ) ;
146- if ( parentSection is not null )
147- {
148- sb . AppendLine ( $ "if (result.{ parentPath } == null)") ;
149- sb . Indent ( ) ;
150- sb . AppendLine ( $ "result.{ parentPath } = new { parentSection . TypeName } ();") ;
151- sb . Outdent ( ) ;
152- sb . AppendLine ( ) ;
153- }
154- }
155- }
156-
157134 for ( int v = 0 ; v < section . Values . Count ; v ++ )
158135 {
159136 ValueInfo value = section . Values [ v ] ;
@@ -220,10 +197,6 @@ private static void AppendWriteMethod(SourceBuilder sb, string className, List<S
220197 if ( s > 0 )
221198 sb . AppendLine ( "sb.AppendLine();" ) ;
222199
223- string nullCheck = BuildNullCheck ( $ "instance.{ section . PropertyPath } ") ;
224- sb . AppendLine ( $ "if ({ nullCheck } )") ;
225- sb . OpenBrace ( ) ;
226-
227200 if ( serializeComments && section . Comment is not null )
228201 sb . AppendLine ( $ "sb.AppendLine(\" ; { GeneratorHelpers . EscapeString ( section . Comment ) } \" );") ;
229202
@@ -236,8 +209,6 @@ private static void AppendWriteMethod(SourceBuilder sb, string className, List<S
236209
237210 sb . AppendLine ( $ "sb.AppendLine(\" { value . KeyName } =\" + { GetToStringExpression ( $ "instance.{ section . PropertyPath } .{ value . PropertyName } ", value ) } );") ;
238211 }
239-
240- sb . CloseBrace ( ) ;
241212 }
242213
243214 sb . AppendLine ( ) ;
@@ -318,15 +289,8 @@ private static void AppendLoadMethod(SourceBuilder sb, string className, List<Se
318289
319290 foreach ( SectionInfo section in sections )
320291 {
321- string targetNullCheck = BuildNullCheck ( $ "this.{ section . PropertyPath } ") ;
322- string sourceNullCheck = BuildNullCheck ( $ "other.{ section . PropertyPath } ") ;
323- sb . AppendLine ( $ "if ({ targetNullCheck } && { sourceNullCheck } )") ;
324- sb . OpenBrace ( ) ;
325-
326292 foreach ( ValueInfo value in section . Values )
327293 sb . AppendLine ( $ "{ section . PropertyPath } .{ value . PropertyName } = other.{ section . PropertyPath } .{ value . PropertyName } ;") ;
328-
329- sb . CloseBrace ( ) ;
330294 }
331295
332296 sb . CloseBrace ( ) ;
@@ -354,7 +318,6 @@ private static List<SectionInfo> GetSections(ClassDeclarationSyntax classDeclara
354318 continue ;
355319
356320 string sectionName = GetExplicitNameOrDefault ( sectionAttribute , propertySymbol . Name ) ;
357- bool needsInit = ! HasInitializer ( classDeclaration , propertySymbol . Name ) ;
358321 string propertyPath = propertySymbol . Name ;
359322 string ? comment = serializeComments ? GetXmlSummaryComment ( propertySymbol ) : null ;
360323
@@ -366,7 +329,6 @@ private static List<SectionInfo> GetSections(ClassDeclarationSyntax classDeclara
366329 PropertyPath : propertyPath ,
367330 SectionName : sectionName ,
368331 TypeName : sectionTypeName ,
369- NeedsInitialization : needsInit ,
370332 Values : values ,
371333 Comment : comment
372334 ) ) ;
@@ -408,10 +370,9 @@ private static void CollectNestedSections(List<SectionInfo> sections, INamedType
408370 PropertyPath : propertyPath ,
409371 SectionName : fullSectionName ,
410372 TypeName : sectionTypeName ,
411- NeedsInitialization : false ,
412373 Values : values ,
413374 Comment : comment
414- ) ) ;
375+ ) ) ;
415376
416377 CollectNestedSections ( sections , sectionType , fullSectionName , propertyPath , sectionDelimiter , depth + 1 , serializeComments ) ;
417378 }
@@ -488,12 +449,19 @@ private static bool HasInitializer(ClassDeclarationSyntax classDeclaration, stri
488449 {
489450 foreach ( MemberDeclarationSyntax member in classDeclaration . Members )
490451 {
491- if ( member is PropertyDeclarationSyntax propSyntax
492- && propSyntax . Identifier . Text == propertyName
493- && propSyntax . Initializer is not null )
494- {
452+ if ( member is PropertyDeclarationSyntax propSyntax && propSyntax . Identifier . Text == propertyName && propSyntax . Initializer is not null )
453+ return true ;
454+ }
455+
456+ return false ;
457+ }
458+
459+ private static bool HasInitializer ( INamedTypeSymbol typeSymbol , string propertyName )
460+ {
461+ foreach ( SyntaxReference syntaxRef in typeSymbol . DeclaringSyntaxReferences )
462+ {
463+ if ( syntaxRef . GetSyntax ( ) is ClassDeclarationSyntax classDeclaration && HasInitializer ( classDeclaration , propertyName ) )
495464 return true ;
496- }
497465 }
498466
499467 return false ;
@@ -575,7 +543,7 @@ private static string GetToStringExpression(string expression, ValueInfo value)
575543 string emptyFallback = value . IsArray
576544 ? $ "Array.Empty<{ value . CollectionElementTypeName } >()"
577545 : $ "Enumerable.Empty<{ value . CollectionElementTypeName } >()";
578-
546+
579547 return $ "string.Join(\" ,\" , ({ expression } ?? { emptyFallback } ).Select(e => { elementToString } ))";
580548 }
581549
@@ -711,19 +679,74 @@ private static bool GetSerializeComments(ClassDeclarationSyntax classDeclaration
711679 return null ;
712680 }
713681
714- private static string BuildNullCheck ( string propertyPath )
682+ private static bool ValidateSectionProperties ( SourceProductionContext context , ClassDeclarationSyntax classDeclaration , SemanticModel semanticModel )
715683 {
716- string [ ] segments = propertyPath . Split ( '.' ) ;
717- if ( segments . Length <= 2 )
718- return $ "{ propertyPath } != null";
684+ INamedTypeSymbol ? classSymbol = semanticModel . GetDeclaredSymbol ( classDeclaration ) ;
685+
686+ if ( classSymbol is null )
687+ return false ;
688+
689+ bool isValid = true ;
690+ ValidateSectionPropertiesOnType ( context , classSymbol , classDeclaration , ref isValid ) ;
691+ return isValid ;
692+ }
719693
720- List < string > checks = [ ] ;
721- for ( int i = 1 ; i < segments . Length ; i ++ )
694+ private static void ValidateSectionPropertiesOnType ( SourceProductionContext context , INamedTypeSymbol typeSymbol , ClassDeclarationSyntax ? classDeclaration , ref bool isValid )
695+ {
696+ foreach ( ISymbol member in typeSymbol . GetMembers ( ) )
722697 {
723- checks . Add ( $ " { string . Join ( "." , segments , 0 , i + 1 ) } != null" ) ;
724- }
698+ if ( member is not IPropertySymbol propertySymbol )
699+ continue ;
725700
726- return string . Join ( " && " , checks ) ;
701+ AttributeData ? sectionAttribute = FindAttribute ( propertySymbol , SectionAttributeName ) ;
702+
703+ if ( sectionAttribute is null )
704+ continue ;
705+
706+ string className = typeSymbol . Name ;
707+ Location location = propertySymbol . Locations . FirstOrDefault ( ) ?? Location . None ;
708+
709+ // Check nullable
710+ if ( propertySymbol . Type . NullableAnnotation == NullableAnnotation . Annotated )
711+ {
712+ context . ReportDiagnostic ( Diagnostic . Create (
713+ DiagnosticDescriptors . IniFileSectionNullablePropertyDiagnostic ,
714+ location ,
715+ propertySymbol . Name ,
716+ className ) ) ;
717+ isValid = false ;
718+ }
719+
720+ // Check initialized
721+ bool hasInit = classDeclaration is not null
722+ ? HasInitializer ( classDeclaration , propertySymbol . Name )
723+ : HasInitializer ( typeSymbol , propertySymbol . Name ) ;
724+
725+ if ( ! hasInit )
726+ {
727+ context . ReportDiagnostic ( Diagnostic . Create (
728+ DiagnosticDescriptors . IniFileSectionUninitializedPropertyDiagnostic ,
729+ location ,
730+ propertySymbol . Name ,
731+ className ) ) ;
732+ isValid = false ;
733+ }
734+
735+ // Check public getter
736+ if ( propertySymbol . GetMethod is null || propertySymbol . GetMethod . DeclaredAccessibility != Accessibility . Public )
737+ {
738+ context . ReportDiagnostic ( Diagnostic . Create (
739+ DiagnosticDescriptors . IniFileSectionNoPublicGetterDiagnostic ,
740+ location ,
741+ propertySymbol . Name ,
742+ className ) ) ;
743+ isValid = false ;
744+ }
745+
746+ // Recursively validate nested section types
747+ if ( propertySymbol . Type is INamedTypeSymbol nestedType )
748+ ValidateSectionPropertiesOnType ( context , nestedType , null , ref isValid ) ;
749+ }
727750 }
728751
729752 private static AttributeData ? FindAttribute ( IPropertySymbol propertySymbol , string attributeName )
@@ -774,7 +797,6 @@ private sealed record SectionInfo(
774797 string PropertyPath ,
775798 string SectionName ,
776799 string TypeName ,
777- bool NeedsInitialization ,
778800 List < ValueInfo > Values ,
779801 string ? Comment = null
780802 ) ;
0 commit comments