Skip to content

Commit 4faec41

Browse files
authored
Remove reflection from NpgsqlConnectionStringBuilder (#3819)
Closes #3814
1 parent a593d1e commit 4faec41

8 files changed

+275
-105
lines changed

src/Npgsql.SourceGenerators/AnalyzerReleases.Shipped.md

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
### New Rules
2+
Rule ID | Category | Severity | Notes
3+
--------|----------|----------|-------
4+
PGXXXX | Internal | Error |

src/Npgsql.SourceGenerators/Npgsql.SourceGenerators.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
5+
<NoWarn>1591</NoWarn>
56
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
67
<IsPackable>false</IsPackable>
78
</PropertyGroup>
@@ -26,5 +27,6 @@
2627

2728
<ItemGroup>
2829
<EmbeddedResource Include="TypeHandler.snbtxt" />
30+
<EmbeddedResource Include="NpgsqlConnectionStringBuilder.snbtxt" />
2931
</ItemGroup>
3032
</Project>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
#nullable disable
5+
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
6+
#pragma warning disable RS0016 // Add public types and members to the declared API
7+
#pragma warning disable 618 // Member is obsolete
8+
9+
namespace Npgsql
10+
{
11+
public sealed partial class NpgsqlConnectionStringBuilder
12+
{
13+
private partial int Init()
14+
{
15+
// Set the strongly-typed properties to their default values
16+
{{
17+
for p in properties
18+
if p.is_obsolete
19+
continue
20+
end
21+
22+
if (p.default_value != null)
23+
}}
24+
{{ p.name }} = {{ p.default_value }};
25+
{{
26+
end
27+
end }}
28+
29+
// Setting the strongly-typed properties here also set the string-based properties in the base class.
30+
// Clear them (default settings = empty connection string)
31+
base.Clear();
32+
33+
return 0;
34+
}
35+
36+
private partial int GeneratedSetter(string keyword, object value)
37+
{
38+
switch (keyword)
39+
{
40+
{{ for kv in properties_by_keyword }}
41+
case "{{ kv.key }}":
42+
{{ p = kv.value }}
43+
{{ if p.is_enum }}
44+
{
45+
{{ p.name }} = value is string s
46+
? ({{ p.type_name }})Enum.Parse(typeof({{ p.type_name }}), s)
47+
: ({{ p.type_name }})Convert.ChangeType(value, typeof({{ p.type_name }}));
48+
}
49+
{{ else }}
50+
{{ p.name }} = ({{ p.type_name }})Convert.ChangeType(value, typeof({{ p.type_name }}));
51+
{{ end }}
52+
break;
53+
{{ end }}
54+
55+
default:
56+
throw new KeyNotFoundException();
57+
}
58+
59+
return 0;
60+
}
61+
62+
private partial bool TryGetValueGenerated(string keyword, out object value)
63+
{
64+
switch (keyword)
65+
{
66+
{{ for kv in properties_by_keyword }}
67+
case "{{ kv.key }}":
68+
{{ p = kv.value }}
69+
value = (object){{ p.name }} ?? "";
70+
return true;
71+
{{ end }}
72+
}
73+
74+
value = null;
75+
return false;
76+
}
77+
78+
private partial bool ContainsKeyGenerated(string keyword)
79+
=> keyword switch
80+
{
81+
{{ for kv in properties_by_keyword }}
82+
"{{ kv.key }}" => true,
83+
{{ end }}
84+
85+
_ => false
86+
};
87+
88+
private partial bool RemoveGenerated(string keyword)
89+
{
90+
switch (keyword)
91+
{
92+
{{ for kv in properties_by_keyword }}
93+
case "{{ kv.key }}":
94+
{
95+
{{ p = kv.value }}
96+
var removed = base.ContainsKey("{{ p.canonical_name }}");
97+
// Note that string property setters call SetValue, which itself calls base.Remove().
98+
{{ if p.default_value == null }}
99+
{{ p.name }} = default;
100+
{{ else }}
101+
{{ p.name }} = {{ p.default_value }};
102+
{{ end }}
103+
base.Remove("{{ p.canonical_name }}");
104+
return removed;
105+
}
106+
{{ end }}
107+
108+
default:
109+
throw new KeyNotFoundException();
110+
}
111+
}
112+
113+
private partial string ToCanonicalKeyword(string keyword)
114+
=> keyword switch
115+
{
116+
{{ for kv in properties_by_keyword }}
117+
"{{ kv.key }}" => "{{ kv.value.canonical_name }}",
118+
{{ end }}
119+
120+
_ => throw new KeyNotFoundException()
121+
};
122+
}
123+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Text;
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.Text;
6+
using Scriban;
7+
8+
namespace Npgsql.SourceGenerators
9+
{
10+
[Generator]
11+
public class NpgsqlConnectionStringBuilderSourceGenerator : ISourceGenerator
12+
{
13+
static readonly DiagnosticDescriptor InternalError = new(
14+
id: "PGXXXX",
15+
title: "Internal issue when source-generating NpgsqlConnectionStringBuilder",
16+
messageFormat: "{0}",
17+
category: "Internal",
18+
DiagnosticSeverity.Error,
19+
isEnabledByDefault: true);
20+
21+
public void Initialize(GeneratorInitializationContext context) {}
22+
23+
public void Execute(GeneratorExecutionContext context)
24+
{
25+
if (context.Compilation.Assembly.GetTypeByMetadataName("Npgsql.NpgsqlConnectionStringBuilder") is not { } type)
26+
return;
27+
28+
if (context.Compilation.Assembly.GetTypeByMetadataName("Npgsql.NpgsqlConnectionStringPropertyAttribute") is not
29+
{ } connectionStringPropertyAttribute)
30+
{
31+
context.ReportDiagnostic(Diagnostic.Create(
32+
InternalError,
33+
location: null,
34+
"Could not find Npgsql.NpgsqlConnectionStringPropertyAttribute"));
35+
return;
36+
}
37+
38+
var obsoleteAttribute = context.Compilation.GetTypeByMetadataName("System.ObsoleteAttribute");
39+
var displayNameAttribute = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DisplayNameAttribute");
40+
var defaultValueAttribute = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DefaultValueAttribute");
41+
42+
if (obsoleteAttribute is null || displayNameAttribute is null || defaultValueAttribute is null)
43+
{
44+
context.ReportDiagnostic(Diagnostic.Create(
45+
InternalError,
46+
location: null,
47+
"Could not find ObsoleteAttribute, DisplayNameAttribute or DefaultValueAttribute"));
48+
return;
49+
}
50+
51+
var properties = new List<PropertyDetails>();
52+
var propertiesByKeyword = new Dictionary<string, PropertyDetails>();
53+
foreach (var member in type.GetMembers())
54+
{
55+
if (member is not IPropertySymbol property ||
56+
property.GetAttributes().FirstOrDefault(a => connectionStringPropertyAttribute.Equals(a.AttributeClass, SymbolEqualityComparer.Default)) is not { } propertyAttribute ||
57+
property.GetAttributes()
58+
.FirstOrDefault(a => displayNameAttribute.Equals(a.AttributeClass, SymbolEqualityComparer.Default))
59+
?.ConstructorArguments[0].Value is not string displayName)
60+
{
61+
continue;
62+
}
63+
64+
var explicitDefaultValue = property.GetAttributes()
65+
.FirstOrDefault(a => defaultValueAttribute.Equals(a.AttributeClass, SymbolEqualityComparer.Default))
66+
?.ConstructorArguments[0].Value;
67+
68+
if (explicitDefaultValue is string s)
69+
explicitDefaultValue = '"' + s.Replace("\"", "\"\"") + '"';
70+
71+
var propertyDetails = new PropertyDetails
72+
{
73+
Name = property.Name,
74+
CanonicalName = displayName,
75+
TypeName = property.Type.Name,
76+
IsEnum = property.Type.TypeKind == TypeKind.Enum,
77+
IsObsolete = property.GetAttributes().Any(a => obsoleteAttribute.Equals(a.AttributeClass, SymbolEqualityComparer.Default)),
78+
DefaultValue = explicitDefaultValue
79+
};
80+
81+
properties.Add(propertyDetails);
82+
83+
propertiesByKeyword[displayName.ToUpperInvariant()] = propertyDetails;
84+
if (property.Name != displayName)
85+
propertiesByKeyword[property.Name.ToUpperInvariant()] = propertyDetails;
86+
if (propertyAttribute.ConstructorArguments.Length == 1)
87+
foreach (var synonymArg in propertyAttribute.ConstructorArguments[0].Values)
88+
if (synonymArg.Value is string synonym)
89+
propertiesByKeyword[synonym.ToUpperInvariant()] = propertyDetails;
90+
}
91+
92+
var template = Template.Parse(EmbeddedResource.GetContent("NpgsqlConnectionStringBuilder.snbtxt"), "NpgsqlConnectionStringBuilder.snbtxt");
93+
94+
var output = template.Render(new
95+
{
96+
Properties = properties,
97+
PropertiesByKeyword = propertiesByKeyword
98+
});
99+
100+
context.AddSource(type.Name + ".Generated.cs", SourceText.From(output, Encoding.UTF8));
101+
}
102+
103+
class PropertyDetails
104+
{
105+
public string Name { get; set; } = null!;
106+
public string CanonicalName { get; set; } = null!;
107+
public string TypeName { get; set; } = null!;
108+
public bool IsEnum { get; set; }
109+
public bool IsObsolete { get; set; }
110+
public object? DefaultValue { get; set; }
111+
}
112+
}
113+
}

src/Npgsql.SourceGenerators/TypeHandlerSourceGenerator.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using Microsoft.CodeAnalysis.CSharp.Syntax;
88
using Microsoft.CodeAnalysis.Text;
99
using Scriban;
10-
using Scriban.Runtime;
1110

1211
namespace Npgsql.SourceGenerators
1312
{

0 commit comments

Comments
 (0)