openai/openai-dotnet

Public

mirrored from https://github.com/openai/openai-dotnetAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
joseharriaga/stable-api-listing

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/Test-ExperimentalAttributes.ps1

437lines · modecode

1<#
2.SYNOPSIS
3 Validates [Experimental] attribute decoration on public APIs in stable classes.
4
5.DESCRIPTION
6 Uses assembly reflection to verify that all public methods and properties in stable classes
7 are either listed in the stable sets (from api/ga-apis.yaml) or decorated
8 with [Experimental]. This catches custom (hand-written) code that is not processed by the
9 code generator's visitor.
10
11.PARAMETER Configuration
12 Build configuration. Defaults to Release.
13
14.PARAMETER TargetFramework
15 Target framework to build and validate. Defaults to the first framework in ClientTargetFrameworks.
16
17.EXAMPLE
18 .\Test-ExperimentalAttributes.ps1
19 Validates using default settings.
20
21.EXAMPLE
22 .\Test-ExperimentalAttributes.ps1 -Configuration Debug -TargetFramework net8.0
23 Validates using Debug configuration targeting net8.0.
24#>
25
26[CmdletBinding()]
27param(
28 [Parameter(Mandatory = $false)]
29 [string]$Configuration = "Release",
30
31 [Parameter(Mandatory = $false)]
32 [string]$TargetFramework
33)
34
35$ErrorActionPreference = "Stop"
36
37$repoRootPath = Join-Path $PSScriptRoot ".." -Resolve
38$projectPath = Join-Path $repoRootPath "src" "OpenAI.csproj"
39$yamlPath = Join-Path $repoRootPath "api" "ga-apis.yaml"
40
41# Auto-detect target framework: pick the highest TFM that is compatible with the
42# current PowerShell/.NET runtime so that the assembly can be loaded for reflection.
43if (-not $TargetFramework) {
44 $runtimeMajor = [System.Environment]::Version.Major
45 $propsPath = Join-Path $repoRootPath "Directory.Build.props"
46 if (Test-Path $propsPath) {
47 $propsContent = Get-Content $propsPath -Raw
48 if ($propsContent -match '<ClientTargetFrameworks>([^<]+)</ClientTargetFrameworks>') {
49 $allFrameworks = ($Matches[1] -split ';') | ForEach-Object { $_.Trim() }
50
51 # Filter to netX.0 frameworks whose major version <= current runtime
52 $compatible = $allFrameworks | Where-Object {
53 $_ -match '^net(\d+)\.0$' -and [int]$Matches[1] -le $runtimeMajor
54 } | Sort-Object { if ($_ -match '(\d+)') { [int]$Matches[1] } } -Descending
55
56 $TargetFramework = $compatible | Select-Object -First 1
57 }
58 }
59 if (-not $TargetFramework) {
60 $TargetFramework = "net8.0"
61 }
62}
63
64Write-Host ""
65Write-Host "Experimental Attribute Validation" -ForegroundColor Cyan
66Write-Host " Configuration: $Configuration" -ForegroundColor DarkGray
67Write-Host " Target Framework: $TargetFramework" -ForegroundColor DarkGray
68Write-Host ""
69
70# ---------------------------------------------------------------------------
71# Step 1: Parse stable sets from api/ga-apis.yaml
72# ---------------------------------------------------------------------------
73
74Write-Host "Parsing stable sets from api/ga-apis.yaml..." -ForegroundColor Cyan
75
76if (-not (Test-Path $yamlPath)) {
77 Write-Error "ga-apis.yaml not found at: $yamlPath"
78 exit 1
79}
80
81$yamlLines = Get-Content $yamlPath
82
83$stableClasses = @()
84$stableProperties = @()
85$stableMethods = @()
86
87$currentSet = $null
88foreach ($line in $yamlLines) {
89 $trimmed = $line.Trim()
90 if ($trimmed.Length -eq 0 -or $trimmed.StartsWith('#')) { continue }
91
92 if ($trimmed -eq 'stableClasses:') { $currentSet = 'classes'; continue }
93 if ($trimmed -eq 'stableProperties:') { $currentSet = 'properties'; continue }
94 if ($trimmed -eq 'stableMethods:') { $currentSet = 'methods'; continue }
95
96 if ($trimmed.StartsWith('- ') -and $currentSet) {
97 $value = $trimmed.Substring(2).Trim()
98 switch ($currentSet) {
99 'classes' { $stableClasses += $value }
100 'properties' { $stableProperties += $value }
101 'methods' { $stableMethods += $value }
102 }
103 }
104}
105
106Write-Host " Stable classes: $($stableClasses.Count)" -ForegroundColor DarkGray
107Write-Host " Stable properties: $($stableProperties.Count)" -ForegroundColor DarkGray
108Write-Host " Stable methods: $($stableMethods.Count)" -ForegroundColor DarkGray
109Write-Host ""
110
111# ---------------------------------------------------------------------------
112# Step 2: Publish the project to a temp directory so all deps are co-located
113# ---------------------------------------------------------------------------
114
115$publishDir = $null
116$violations = @()
117
118$validatorCode = @'
119using System;
120using System.Collections.Generic;
121using System.IO;
122using System.Linq;
123using System.Reflection;
124using System.Runtime.Loader;
125
126public class ExperimentalAttributeValidator
127{
128 private class ValidatorLoadContext : AssemblyLoadContext
129 {
130 private readonly string _basePath;
131
132 public ValidatorLoadContext(string basePath) : base("ExperimentalValidator", isCollectible: true)
133 {
134 _basePath = basePath;
135 }
136
137 protected override Assembly Load(AssemblyName assemblyName)
138 {
139 string path = Path.Combine(_basePath, assemblyName.Name + ".dll");
140 if (File.Exists(path))
141 return LoadFromAssemblyPath(path);
142 return null;
143 }
144 }
145
146 private static readonly Dictionary<string, string> TypeAliases = new(StringComparer.Ordinal)
147 {
148 { "Boolean", "bool" },
149 { "Byte", "byte" },
150 { "Char", "char" },
151 { "Decimal", "decimal" },
152 { "Double", "double" },
153 { "Int16", "short" },
154 { "Int32", "int" },
155 { "Int64", "long" },
156 { "Object", "object" },
157 { "SByte", "sbyte" },
158 { "Single", "float" },
159 { "String", "string" },
160 { "UInt16", "ushort" },
161 { "UInt32", "uint" },
162 { "UInt64", "ulong" },
163 { "Void", "void" },
164 };
165
166 public static string GetFriendlyTypeName(Type type)
167 {
168 if (type.IsByRef)
169 return GetFriendlyTypeName(type.GetElementType());
170
171 if (type.IsArray)
172 return GetFriendlyTypeName(type.GetElementType()) + "[]";
173
174 if (type.IsGenericType)
175 {
176 Type underlying = Nullable.GetUnderlyingType(type);
177 if (underlying != null)
178 return GetFriendlyTypeName(underlying) + "?";
179
180 string baseName = type.Name.Substring(0, type.Name.IndexOf('`'));
181 string args = string.Join(", ", type.GetGenericArguments().Select(GetFriendlyTypeName));
182 return baseName + "<" + args + ">";
183 }
184
185 if (TypeAliases.TryGetValue(type.Name, out var alias))
186 return alias;
187
188 return type.Name;
189 }
190
191 public static string GetMethodLookupName(MethodInfo method, string typeName)
192 {
193 var paramTypes = method.GetParameters()
194 .Select(p => GetFriendlyTypeName(p.ParameterType))
195 .ToArray();
196
197 string name;
198 switch (method.Name)
199 {
200 case "op_Implicit":
201 name = "operator implicit " + typeName;
202 break;
203 case "op_Explicit":
204 name = "operator explicit " + typeName;
205 break;
206 case "op_Equality":
207 name = "operator ==";
208 break;
209 case "op_Inequality":
210 name = "operator !=";
211 break;
212 case "op_GreaterThan":
213 name = "operator >";
214 break;
215 case "op_LessThan":
216 name = "operator <";
217 break;
218 case "op_GreaterThanOrEqual":
219 name = "operator >=";
220 break;
221 case "op_LessThanOrEqual":
222 name = "operator <=";
223 break;
224 case "op_Addition":
225 name = "operator +";
226 break;
227 case "op_Subtraction":
228 name = "operator -";
229 break;
230 default:
231 name = method.Name;
232 break;
233 }
234
235 if (paramTypes.Length > 0)
236 return typeName + "." + name + "|" + string.Join("|", paramTypes);
237 return typeName + "." + name;
238 }
239
240 public static bool HasExperimentalAttribute(MemberInfo member)
241 {
242 try
243 {
244 return member.GetCustomAttributesData()
245 .Any(a => a.AttributeType.Name == "ExperimentalAttribute");
246 }
247 catch
248 {
249 return true;
250 }
251 }
252
253 public static string[] Validate(
254 string dllPath,
255 string[] stableClasses,
256 string[] stableProperties,
257 string[] stableMethods)
258 {
259 var stableClassSet = new HashSet<string>(stableClasses, StringComparer.OrdinalIgnoreCase);
260 var stablePropSet = new HashSet<string>(stableProperties, StringComparer.OrdinalIgnoreCase);
261 var stableMethodSet = new HashSet<string>(stableMethods, StringComparer.OrdinalIgnoreCase);
262
263 var violations = new List<string>();
264 var basePath = Path.GetDirectoryName(Path.GetFullPath(dllPath));
265
266 var ctx = new ValidatorLoadContext(basePath);
267 Assembly assembly = null;
268 Type[] types = Array.Empty<Type>();
269 try
270 {
271 try
272 {
273 assembly = ctx.LoadFromAssemblyPath(Path.GetFullPath(dllPath));
274 }
275 catch (Exception ex)
276 {
277 return new[] { "ERROR|Assembly load failed|" + ex.Message + "|" };
278 }
279
280 var bindingFlags = BindingFlags.Public | BindingFlags.Instance |
281 BindingFlags.Static | BindingFlags.DeclaredOnly;
282
283 try
284 {
285 types = assembly.GetExportedTypes();
286 }
287 catch (ReflectionTypeLoadException ex)
288 {
289 types = ex.Types.Where(t => t != null).ToArray();
290 }
291
292 foreach (var type in types)
293 {
294 string fullName = type.Namespace + "." + type.Name;
295 if (!stableClassSet.Contains(fullName))
296 continue;
297
298 string typeName = type.Name;
299
300 // Validate properties
301 try
302 {
303 foreach (var prop in type.GetProperties(bindingFlags))
304 {
305 string lookupName = typeName + "." + prop.Name;
306 if (!stablePropSet.Contains(lookupName) && !HasExperimentalAttribute(prop))
307 {
308 violations.Add("PROPERTY|" + fullName + "|" + prop.Name + "|" + lookupName);
309 }
310 }
311 }
312 catch { /* skip types whose properties can't be reflected */ }
313
314 // Validate methods (skip property/event accessors)
315 try
316 {
317 foreach (var method in type.GetMethods(bindingFlags))
318 {
319 if (method.IsSpecialName &&
320 (method.Name.StartsWith("get_") || method.Name.StartsWith("set_") ||
321 method.Name.StartsWith("add_") || method.Name.StartsWith("remove_")))
322 continue;
323
324 string lookupName = GetMethodLookupName(method, typeName);
325 if (!stableMethodSet.Contains(lookupName) && !HasExperimentalAttribute(method))
326 {
327 var paramDesc = string.Join(", ",
328 method.GetParameters().Select(p => GetFriendlyTypeName(p.ParameterType)));
329 violations.Add("METHOD|" + fullName + "|" +
330 method.Name + "(" + paramDesc + ")|" + lookupName);
331 }
332 }
333 }
334 catch { /* skip types whose methods can't be reflected */ }
335 }
336
337 return violations.ToArray();
338 }
339 finally
340 {
341 types = Array.Empty<Type>();
342 assembly = null;
343 ctx.Unload();
344 GC.Collect();
345 GC.WaitForPendingFinalizers();
346 GC.Collect();
347 }
348 }
349}
350'@
351
352try {
353 $publishDir = Join-Path ([System.IO.Path]::GetTempPath()) "openai-experimental-validation-$([System.IO.Path]::GetRandomFileName())"
354
355 Write-Host "Publishing project ($TargetFramework)..." -ForegroundColor Cyan
356
357 & dotnet publish $projectPath -c $Configuration -f $TargetFramework -o $publishDir --nologo -v quiet 2>&1 | Out-Null
358
359 if ($LASTEXITCODE -ne 0) {
360 & dotnet publish $projectPath -c $Configuration -f $TargetFramework -o $publishDir --nologo
361 throw "Build/publish failed with exit code $LASTEXITCODE"
362 }
363
364 $dllPath = Join-Path $publishDir "OpenAI.dll"
365 if (-not (Test-Path $dllPath)) {
366 throw "Expected assembly not found at: $dllPath"
367 }
368
369 Write-Host ""
370
371 # ---------------------------------------------------------------------------
372 # Step 3: Compile and run the reflection-based validator
373 # ---------------------------------------------------------------------------
374
375 Write-Host "Loading assembly and validating..." -ForegroundColor Cyan
376
377 $validatorType = [AppDomain]::CurrentDomain.GetAssemblies() |
378 ForEach-Object { $_.GetType('ExperimentalAttributeValidator', $false, $false) } |
379 Where-Object { $null -ne $_ } |
380 Select-Object -First 1
381
382 if ($null -eq $validatorType) {
383 Add-Type -TypeDefinition $validatorCode -WarningAction SilentlyContinue
384 }
385
386 # ---------------------------------------------------------------------------
387 # Step 4: Run validation
388 # ---------------------------------------------------------------------------
389
390 $violations = [ExperimentalAttributeValidator]::Validate(
391 $dllPath,
392 [string[]]$stableClasses,
393 [string[]]$stableProperties,
394 [string[]]$stableMethods
395 )
396}
397catch {
398 Write-Error $_
399 exit 1
400}
401finally {
402 if ($publishDir -and (Test-Path $publishDir)) {
403 Remove-Item -Path $publishDir -Recurse -Force -ErrorAction SilentlyContinue
404 }
405}
406
407# ---------------------------------------------------------------------------
408# Step 5: Report results
409# ---------------------------------------------------------------------------
410
411if ($violations.Count -eq 0) {
412 Write-Host ""
413 Write-Host "All public APIs in stable classes are correctly attributed." -ForegroundColor Green
414 Write-Host ""
415 exit 0
416}
417
418Write-Host ""
419Write-Host "Found $($violations.Count) violation(s) - public members in stable classes missing [Experimental] attribute:" -ForegroundColor Red
420Write-Host ""
421
422foreach ($v in $violations) {
423 $parts = $v -split '\|', 4
424 $kind = $parts[0]
425 $class = $parts[1]
426 $member = $parts[2]
427 $lookup = $parts[3]
428
429 Write-Host " [$kind] $class :: $member" -ForegroundColor Yellow
430 Write-Host " Lookup key: $lookup" -ForegroundColor DarkGray
431}
432
433Write-Host ""
434Write-Host "To fix: add [Experimental(""OPENAI001"")] to these members in custom code," -ForegroundColor Red
435Write-Host "or add them to the stable sets in api/ga-apis.yaml." -ForegroundColor Red
436Write-Host ""
437exit 1
438