< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.OpenApi.OpenApiComponentAnnotationScanner
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiComponentAnnotationScanner.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
48%
Covered lines: 297
Uncovered lines: 316
Coverable lines: 613
Total lines: 1849
Line coverage: 48.4%
Branch coverage
36%
Covered branches: 218
Total branches: 590
Branch coverage: 36.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 01/08/2026 - 02:20:28 Line coverage: 44.9% (245/545) Branch coverage: 31.7% (167/526) Total lines: 1636 Tag: Kestrun/Kestrun@4bc17b7e465c315de6386907c417e44fcb0fd3eb01/09/2026 - 06:56:42 Line coverage: 48% (291/606) Branch coverage: 36.5% (212/580) Total lines: 1836 Tag: Kestrun/Kestrun@94f8107dc592fa7eaec45c0dd5f9fffbd41bc14501/12/2026 - 18:03:06 Line coverage: 48.3% (293/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/12/2026 - 22:21:01 Line coverage: 48.6% (295/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@91f4d7da7b838286aa6f574ec486cbb057167d6401/14/2026 - 16:24:22 Line coverage: 48.3% (293/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@e6894577f8be1c47294e9e3a795b9d779938e17501/14/2026 - 21:08:55 Line coverage: 48.6% (295/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@b584423d949a5193ed8cd45cf9df490f06d1c54501/16/2026 - 03:52:31 Line coverage: 48.3% (293/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@0077556dd757d1b7434cf700cd5c7be05cea351401/17/2026 - 04:33:35 Line coverage: 48.6% (295/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/17/2026 - 18:18:02 Line coverage: 48.3% (293/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@8dd16f7908c0e15b594d16bb49be0240e2c7c01801/18/2026 - 06:40:41 Line coverage: 48.6% (295/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@99e92690d0fd95f6f4896f3410d2c024350a979401/18/2026 - 21:37:07 Line coverage: 48.4% (297/613) Branch coverage: 36.9% (218/590) Total lines: 1849 Tag: Kestrun/Kestrun@99c4ae445e8e5afc8b7080e01d5d9cdf39f972b801/19/2026 - 18:47:02 Line coverage: 48.1% (295/613) Branch coverage: 36.9% (218/590) Total lines: 1849 Tag: Kestrun/Kestrun@716db6917075bf04d6f8ae45a1bad48ca5cfacfe01/26/2026 - 05:11:18 Line coverage: 48.4% (297/613) Branch coverage: 36.9% (218/590) Total lines: 1849 Tag: Kestrun/Kestrun@698a353442ed1dc2deec89391a324a20b0f74e0b 01/08/2026 - 02:20:28 Line coverage: 44.9% (245/545) Branch coverage: 31.7% (167/526) Total lines: 1636 Tag: Kestrun/Kestrun@4bc17b7e465c315de6386907c417e44fcb0fd3eb01/09/2026 - 06:56:42 Line coverage: 48% (291/606) Branch coverage: 36.5% (212/580) Total lines: 1836 Tag: Kestrun/Kestrun@94f8107dc592fa7eaec45c0dd5f9fffbd41bc14501/12/2026 - 18:03:06 Line coverage: 48.3% (293/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/12/2026 - 22:21:01 Line coverage: 48.6% (295/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@91f4d7da7b838286aa6f574ec486cbb057167d6401/14/2026 - 16:24:22 Line coverage: 48.3% (293/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@e6894577f8be1c47294e9e3a795b9d779938e17501/14/2026 - 21:08:55 Line coverage: 48.6% (295/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@b584423d949a5193ed8cd45cf9df490f06d1c54501/16/2026 - 03:52:31 Line coverage: 48.3% (293/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@0077556dd757d1b7434cf700cd5c7be05cea351401/17/2026 - 04:33:35 Line coverage: 48.6% (295/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/17/2026 - 18:18:02 Line coverage: 48.3% (293/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@8dd16f7908c0e15b594d16bb49be0240e2c7c01801/18/2026 - 06:40:41 Line coverage: 48.6% (295/606) Branch coverage: 37.4% (217/580) Total lines: 1836 Tag: Kestrun/Kestrun@99e92690d0fd95f6f4896f3410d2c024350a979401/18/2026 - 21:37:07 Line coverage: 48.4% (297/613) Branch coverage: 36.9% (218/590) Total lines: 1849 Tag: Kestrun/Kestrun@99c4ae445e8e5afc8b7080e01d5d9cdf39f972b801/19/2026 - 18:47:02 Line coverage: 48.1% (295/613) Branch coverage: 36.9% (218/590) Total lines: 1849 Tag: Kestrun/Kestrun@716db6917075bf04d6f8ae45a1bad48ca5cfacfe01/26/2026 - 05:11:18 Line coverage: 48.4% (297/613) Branch coverage: 36.9% (218/590) Total lines: 1849 Tag: Kestrun/Kestrun@698a353442ed1dc2deec89391a324a20b0f74e0b

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Annotations()100%11100%
get_Name()100%11100%
get_VariableType()100%11100%
get_VariableTypeName()100%11100%
get_InitialValue()100%11100%
get_InitialValueExpression()100%11100%
get_NoDefault()100%11100%
ScanFromRunningScriptOrPath(...)0%2040%
ScanFromPath(...)90%101094.11%
ParseFile(...)100%11100%
FindDotSourcedFiles()83.33%66100%
ResolveDotSourcedFilesFromCommand()50%171058.33%
TryResolveDotSourcedPathFromElements(...)50%4475%
TryGetSimpleString(...)50%5466.66%
ResolveDotSourcedPath(...)50%4472.72%
ExtractAnnotationsFromAst(...)94.44%181891.66%
GetNamedBlocks(...)66.66%7675%
GetTopLevelStatements(...)100%22100%
TryHandleInlineAttributedAssignment(...)94.44%181894.73%
TryHandleInlineAttributedDeclaration(...)100%1010100%
TryHandleStandaloneAttributeLine(...)16.66%13642.85%
TryHandleVariableAssignment(...)12.5%48814.28%
ApplyVariableTypeInfo(...)0%2040%
ApplyInitializerValues(...)0%4260%
ApplyPendingAnnotations(...)0%4260%
TryHandleDeclarationOnlyVariable(...)7.14%1421413.33%
GetOrCreateVariable(...)100%22100%
TryExtractInlineAttributedDeclaration(...)62.5%9880%
TryExtractExpressionFromStatement(...)90%101085.71%
CollectMatchingAttributesFromChain(...)100%66100%
TryExtractInlineAttributedAssignment(...)80%101088.23%
TryGetAssignmentTarget(...)100%210%
TryGetDeclaredVariableInfo(...)90%101092.3%
UnwrapAttributedExpressionChain(...)75%88100%
TryUnwrapConvertExpression(...)50%3250%
UnwrapRemainingAttributedExpressions(...)50%3250%
TryInferVariableTypeFromAttributes(...)50%7672.72%
ResolvePowerShellTypeName(...)3.7%15915419.23%
NormalizePowerShellTypeName(...)0%156120%
EvaluateValueStatement(...)77.77%1818100%
TryCreateAnnotation(...)50%2260%
TryCreateKestrunAnnotation(...)33.33%451856.25%
TryGetConstantLikeValue(...)0%7280%
TryGetPlainConstantValue(...)0%620%
TryGetPlainStringValue(...)0%620%
TryGetStaticTypeMemberValue(...)0%210140%
TryGetReflectedPropertyValue(...)0%620%
TryGetReflectedFieldValue(...)0%620%
GetMemberName(...)0%7280%
TryCmdletMetadataAttribute(...)0%4160640%
ApplyDefaultComponentName(...)40%261045.45%
ApplyNamedArgument(...)66.66%281252.38%
EvaluateArgumentExpression(...)40%492058.33%
EvaluateParenExpression(...)50%10866.66%
EvaluateVariableExpression(...)0%4260%
TryEvaluateMemberExpression(...)0%110100%
ConvertToPropertyType(...)50%361657.14%
ConvertArrayValue(...)42.85%661435.71%
TryParseStringList(...)0%156120%
ConvertEnumerableToArray(...)100%44100%
ConvertEnumValue(...)50%22100%
TryParseBooleanString(...)0%7280%
ChangeTypeOrRaw(...)100%210%
ResolveKestrunAnnotationType(...)83.33%1212100%
ResolveCmdletMetadataAttributeType(...)0%110100%
ResolveTypeFromName(...)75%141276.47%
IsMatchingAttribute(...)50%6450%
TryParseStandaloneAttributeLine(...)15%1392033.33%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiComponentAnnotationScanner.cs

#LineLine coverage
 1using System.Management.Automation;
 2using System.Management.Automation.Language;
 3using System.Reflection;
 4using System.Collections;
 5using System.Management.Automation.Internal;
 6
 7namespace Kestrun.OpenApi;
 8
 9/// <summary>
 10/// Scans PowerShell script files for OpenAPI component annotations defined via attributes.
 11/// </summary>
 12public static class OpenApiComponentAnnotationScanner
 13{
 14    /// <summary>
 15    /// Represents a variable discovered in script, along with its OpenAPI annotations and metadata.
 16    /// </summary>
 717    public sealed class AnnotatedVariable(string name)
 18    {
 19        /// <summary>Annotations attached to the variable.</summary>
 2720        public List<KestrunAnnotation> Annotations { get; } = [];
 21
 22        /// <summary>
 23        /// The variable name.
 24        /// </summary>
 1325        public string Name { get; set; } = name;
 26
 27        /// <summary>The declared variable type if present (e.g. from <c>[int]$x</c> or <c>[int]$x = 1</c>).</summary>
 1928        public Type? VariableType { get; set; }
 29
 30        /// <summary>The declared variable type name as written in script (best-effort).</summary>
 831        public string? VariableTypeName { get; set; }
 32
 33        /// <summary>The initializer value if it can be evaluated (best-effort).</summary>
 834        public object? InitialValue { get; set; }
 35
 36        /// <summary>The initializer expression text (always available when an initializer exists).</summary>
 237        public string? InitialValueExpression { get; set; }
 38        /// <summary>Indicates whether the variable was declared with no default (e.g. <c>$x = [NoDefault]</c>).</summar
 1139        public bool NoDefault { get; internal set; }
 40    }
 41
 42    /// <summary>
 43    /// Scan starting from the running script (via $PSCommandPath) or a provided mainPath,
 44    /// follow dot-sourced files, and extract "standalone attribute line applies to next variable assignment" annotation
 45    /// </summary>
 46    /// <param name="engine">The PowerShell engine intrinsics.</param>
 47    /// <param name="mainPath">Optional main script file path to start scanning from. If null, uses the running script p
 48    /// <param name="attributeTypeFilter">Optional filter of attribute type names to include. If null or empty, includes
 49    /// <param name="componentNameArgument">The name of the attribute argument to use as component name.</param>
 50    /// <param name="maxFiles">Maximum number of files to scan to prevent cycles.</param>
 51    /// <exception cref="InvalidOperationException">Thrown if no running script path is found and no mainPath is provide
 52    /// <returns>
 53    /// A dictionary keyed by variable name. Each value contains the collected annotations,
 54    /// the declared variable type (if any), and the initializer value/expression (if any).
 55    /// </returns>
 56    /// <remarks>
 57    /// This method performs a breadth-first traversal of the script files starting from mainPath or $PSCommandPath,
 58    /// following dot-sourced includes and extracting attribute annotations that precede variable assignments.
 59    /// </remarks>
 60    public static Dictionary<string, AnnotatedVariable> ScanFromRunningScriptOrPath(
 61        EngineIntrinsics engine,
 62        string? mainPath = null,
 63        IReadOnlyCollection<string>? attributeTypeFilter = null,
 64        string componentNameArgument = "Name",
 65        int maxFiles = 200)
 66    {
 67        // Prefer running script path if available
 068        var psCommandPath = engine.SessionState.PSVariable.GetValue("PSCommandPath") as string;
 069        var entry = mainPath ?? psCommandPath;
 70
 071        if (string.IsNullOrWhiteSpace(entry))
 72        {
 073            throw new InvalidOperationException("No running script path found ($PSCommandPath is empty) and no mainPath 
 74        }
 75
 076        entry = Path.GetFullPath(entry);
 77
 078        return ScanFromPath(entry, attributeTypeFilter, componentNameArgument, maxFiles);
 79    }
 80
 81    /// <summary>
 82    /// Scan starting from a main script file path.
 83    /// </summary>
 84    /// <param name="mainPath">The main script file path to start scanning from.</param>
 85    /// <param name="attributeTypeFilter">Optional filter of attribute type names to include. If null or empty, includes
 86    /// <param name="componentNameArgument">The name of the attribute argument to use as component name.</param>
 87    /// <param name="maxFiles">Maximum number of files to scan to prevent cycles.</param>
 88    /// <returns>
 89    /// A dictionary keyed by variable name. Each value contains the collected annotations,
 90    /// the declared variable type (if any), and the initializer value/expression (if any).
 91    /// </returns>
 92    /// <exception cref="InvalidOperationException">Thrown if the maximum number of files is exceeded while following do
 93    /// <remarks>
 94    /// This method performs a breadth-first traversal of the script files starting from mainPath,
 95    /// following dot-sourced includes and extracting attribute annotations that precede variable assignments.
 96    /// </remarks>
 97    public static Dictionary<string, AnnotatedVariable> ScanFromPath(
 98        string mainPath,
 99        IReadOnlyCollection<string>? attributeTypeFilter = null,
 100        string componentNameArgument = "Name",
 101        int maxFiles = 200)
 102    {
 3103        mainPath = Path.GetFullPath(mainPath);
 104
 3105        var variables = new Dictionary<string, AnnotatedVariable>(StringComparer.OrdinalIgnoreCase);
 106
 3107        var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 3108        var queue = new Queue<string>();
 109
 3110        _ = visited.Add(mainPath);
 3111        queue.Enqueue(mainPath);
 112
 7113        while (queue.Count > 0)
 114        {
 4115            if (visited.Count > maxFiles)
 116            {
 0117                throw new InvalidOperationException($"Exceeded maxFiles={maxFiles} while following dot-sourced scripts. 
 118            }
 119
 4120            var file = queue.Dequeue();
 4121            var ast = ParseFile(file, out _);
 122
 123            // If you care, you can log parse errors:
 124            // foreach (var e in errors) Console.Error.WriteLine($"{file}:{e.Extent.StartLineNumber}:{e.Message}");
 125
 4126            ExtractAnnotationsFromAst(ast, variables, attributeTypeFilter, componentNameArgument);
 127
 10128            foreach (var inc in FindDotSourcedFiles(ast, file))
 129            {
 1130                if (inc is null)
 131                {
 132                    continue;
 133                }
 134
 1135                if (visited.Add(inc))
 136                {
 1137                    queue.Enqueue(inc);
 138                }
 139            }
 140        }
 141
 3142        return variables;
 143    }
 144
 145    // ---------------- Parsing ----------------
 146
 147    /// <summary>
 148    /// Parses a PowerShell script file into a ScriptBlockAst.
 149    /// </summary>
 150    /// <param name="path">The file path of the PowerShell script to parse.</param>
 151    /// <param name="errors">Output array of parse errors encountered during parsing.</param>
 152    /// <returns>The parsed ScriptBlockAst representing the script's abstract syntax tree.</returns>
 153    private static ScriptBlockAst ParseFile(string path, out ParseError[] errors)
 154    {
 4155        var text = File.ReadAllText(path);
 4156        var ast = Parser.ParseInput(text, out _, out errors);
 157        // NOTE: ast.Extent.File is often null here; we carry file path ourselves when resolving includes.
 4158        return ast;
 159    }
 160
 161    // ---------------- Dot-sourcing discovery ----------------
 162
 163    /// <summary>
 164    /// Finds dot-sourcing statements: . ./file.ps1, . "$PSScriptRoot\file.ps1", . ".\file.ps1"
 165    /// Best-effort: resolves simple literal/expandable strings only.
 166    /// </summary>
 167    /// <param name="ast">The ScriptBlockAst to search for dot-sourcing commands.</param>
 168    /// <param name="currentFilePath">The current script file path to resolve relative includes.</param>
 169    /// <returns>An enumerable of resolved file paths that are dot-sourced.</returns>
 170    private static IEnumerable<string?> FindDotSourcedFiles(ScriptBlockAst ast, string currentFilePath)
 171    {
 4172        var baseDir = Path.GetDirectoryName(currentFilePath) ?? Directory.GetCurrentDirectory();
 173
 174        var commands = ast.FindAll(n => n is CommandAst, searchNestedScriptBlocks: true)
 4175            .Cast<CommandAst>();
 176
 14177        foreach (var cmd in commands)
 178        {
 8179            foreach (var resolved in ResolveDotSourcedFilesFromCommand(cmd, baseDir))
 180            {
 1181                yield return resolved;
 182            }
 183        }
 4184    }
 185
 186    /// <summary>
 187    /// Resolves dot-sourced file paths from a PowerShell <see cref="CommandAst"/>.
 188    /// </summary>
 189    /// <param name="cmd">The command AST node to inspect.</param>
 190    /// <param name="baseDir">The base directory used to resolve relative paths.</param>
 191    /// <returns>Zero or more resolved dot-sourced file paths.</returns>
 192    private static IEnumerable<string?> ResolveDotSourcedFilesFromCommand(CommandAst cmd, string baseDir)
 193    {
 194        // Dot-sourcing in the PowerShell AST is represented via InvocationOperator = TokenKind.Dot,
 195        // and the dot token is NOT part of CommandElements.
 196        // Example: . "$PSScriptRoot\\a.ps1" => CommandElements[0] is the path expression.
 3197        if (cmd.InvocationOperator == TokenKind.Dot)
 198        {
 1199            if (TryResolveDotSourcedPathFromElements(cmd.CommandElements, elementIndex: 0, baseDir, out var resolved))
 200            {
 1201                yield return resolved;
 202            }
 203
 1204            yield break;
 205        }
 206
 207        // Back-compat / best-effort: some AST shapes could represent '.' as a command element.
 2208        var elems = cmd.CommandElements;
 2209        if (elems.Count < 2)
 210        {
 2211            yield break;
 212        }
 213
 0214        if (!string.Equals(elems[0].Extent.Text.Trim(), ".", StringComparison.Ordinal))
 215        {
 0216            yield break;
 217        }
 218
 0219        if (TryResolveDotSourcedPathFromElements(elems, elementIndex: 1, baseDir, out var resolvedCompat))
 220        {
 0221            yield return resolvedCompat;
 222        }
 0223    }
 224
 225    /// <summary>
 226    /// Tries to resolve a dot-sourced file path from a specific command element.
 227    /// </summary>
 228    /// <param name="elements">The command elements to read from.</param>
 229    /// <param name="elementIndex">The element index expected to contain the dot-sourced path expression.</param>
 230    /// <param name="baseDir">The base directory used to resolve relative paths.</param>
 231    /// <param name="resolved">The resolved full file path, if available and exists.</param>
 232    /// <returns><c>true</c> if a file path was resolved and exists; otherwise <c>false</c>.</returns>
 233    private static bool TryResolveDotSourcedPathFromElements(IReadOnlyList<CommandElementAst> elements, int elementIndex
 234    {
 1235        resolved = null;
 236
 1237        if (elements.Count <= elementIndex)
 238        {
 0239            return false;
 240        }
 241
 1242        var raw = TryGetSimpleString(elements[elementIndex]);
 1243        if (raw is null)
 244        {
 0245            return false;
 246        }
 247
 1248        resolved = ResolveDotSourcedPath(raw, baseDir);
 1249        return resolved is not null;
 250    }
 251
 252    /// <summary>
 253    /// Tries to extract a simple string value from a CommandElementAst.
 254    /// </summary>
 255    /// <param name="element">The CommandElementAst to extract the string from.</param>
 256    /// <returns>The extracted string if successful; otherwise, null.</returns>
 257    private static string? TryGetSimpleString(CommandElementAst element)
 258    {
 259        // Handles: ". './a.ps1'" or ". \"$PSScriptRoot\\a.ps1\""
 1260        return element switch
 1261        {
 0262            StringConstantExpressionAst s => s.Value,
 1263            ExpandableStringExpressionAst e => e.Value,// contains "$PSScriptRoot\foo.ps1" as text; we do best-effort re
 0264            _ => null,
 1265        };
 266    }
 267
 268    /// <summary>
 269    /// Resolves a dot-sourced file path, handling common tokens and relative paths.
 270    /// </summary>
 271    /// <param name="raw">The raw file path string from the dot-sourcing statement.</param>
 272    /// <param name="baseDir">The base directory to resolve relative paths against.</param>
 273    /// <returns>The resolved full file path if it exists; otherwise, null.</returns>
 274    private static string? ResolveDotSourcedPath(string raw, string baseDir)
 275    {
 1276        var t = raw.Trim();
 277
 278        // Expand the common tokens best-effort (no general expression evaluation!)
 1279        t = t.Replace("$PSScriptRoot", baseDir, StringComparison.OrdinalIgnoreCase);
 1280        t = t.Replace("$PWD", Directory.GetCurrentDirectory(), StringComparison.OrdinalIgnoreCase);
 281
 282        // Normalize path separators to the platform-appropriate separator
 283        // This handles cases where PowerShell code uses backslashes on Unix-like systems
 1284        t = t.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar);
 285
 286        // Relative -> baseDir
 1287        if (!Path.IsPathRooted(t))
 288        {
 0289            t = Path.Combine(baseDir, t);
 290        }
 291
 292        try
 293        {
 1294            var full = Path.GetFullPath(t);
 1295            return File.Exists(full) ? full : null;
 296        }
 0297        catch
 298        {
 0299            return null;
 300        }
 1301    }
 302
 303    // ---------------- Annotation extraction ----------------
 304    /// <summary>
 305    /// Extracts attribute annotations from a ScriptBlockAst and populates the provided dictionary.
 306    /// </summary>
 307    /// <param name="scriptAst">The ScriptBlockAst to extract annotations from.</param>
 308    /// <param name="variables">The dictionary to populate with extracted annotations.</param>
 309    /// <param name="attributeTypeFilter">Optional filter for attribute types to include.</param>
 310    /// <param name="componentNameArgument">The argument name to use for component names.</param>
 311    /// <param name="strict">If true, clears pending annotations on non-matching statements.</param>
 312    private static void ExtractAnnotationsFromAst(
 313        ScriptBlockAst scriptAst,
 314        Dictionary<string, AnnotatedVariable> variables,
 315        IReadOnlyCollection<string>? attributeTypeFilter,
 316        string componentNameArgument,
 317        bool strict = true)
 318    {
 319        // We want statements in lexical order; easiest is to walk each NamedBlock and sort by offsets.
 16320        foreach (var block in GetNamedBlocks(scriptAst))
 321        {
 4322            var statements = GetTopLevelStatements(block);
 323
 4324            var pending = new List<AttributeAst>();
 325
 18326            foreach (var st in statements)
 327            {
 5328                if (TryHandleInlineAttributedAssignment(st, variables, attributeTypeFilter, componentNameArgument, pendi
 329                {
 330                    continue;
 331                }
 332
 2333                if (TryHandleInlineAttributedDeclaration(st, variables, attributeTypeFilter, componentNameArgument, pend
 334                {
 335                    continue;
 336                }
 337
 1338                if (TryHandleStandaloneAttributeLine(st, attributeTypeFilter, pending))
 339                {
 340                    continue;
 341                }
 342
 1343                if (TryHandleVariableAssignment(st, variables, componentNameArgument, pending))
 344                {
 345                    continue;
 346                }
 347
 1348                if (TryHandleDeclarationOnlyVariable(st, variables, componentNameArgument, pending))
 349                {
 350                    continue;
 351                }
 352
 353                // strict: anything else clears pending
 1354                if (strict && pending.Count > 0)
 355                {
 0356                    pending.Clear();
 357                }
 358            }
 359        }
 4360    }
 361
 362    /// <summary>
 363    /// Gets the named blocks (<c>begin</c>, <c>process</c>, <c>end</c>) from a script AST.
 364    /// </summary>
 365    /// <param name="scriptAst">The script AST to inspect.</param>
 366    /// <returns>A list of non-null named blocks.</returns>
 367    private static List<Ast> GetNamedBlocks(ScriptBlockAst scriptAst)
 368    {
 4369        var blocks = new List<Ast>();
 370
 4371        if (scriptAst.BeginBlock is not null)
 372        {
 0373            blocks.Add(scriptAst.BeginBlock);
 374        }
 375
 4376        if (scriptAst.ProcessBlock is not null)
 377        {
 0378            blocks.Add(scriptAst.ProcessBlock);
 379        }
 380
 4381        if (scriptAst.EndBlock is not null)
 382        {
 4383            blocks.Add(scriptAst.EndBlock);
 384        }
 385
 4386        return blocks;
 387    }
 388
 389    /// <summary>
 390    /// Returns the top-level statements for a named block, ordered by lexical position.
 391    /// </summary>
 392    /// <param name="block">The named block AST node.</param>
 393    /// <returns>A list of statements in lexical order.</returns>
 394    private static List<StatementAst> GetTopLevelStatements(Ast block)
 66395        => [.. block.FindAll(n => n is StatementAst sa && sa.Parent == block, searchNestedScriptBlocks: false)
 4396            .Cast<StatementAst>()
 9397            .OrderBy(s => s.Extent.StartOffset)];
 398
 399    /// <summary>
 400    /// Handles inline attributed assignments, e.g. <c>[Attr()][int]$x = 1</c>, attaching annotations and defaults.
 401    /// </summary>
 402    /// <param name="statement">The statement to inspect.</param>
 403    /// <param name="variables">The variable map to populate.</param>
 404    /// <param name="attributeTypeFilter">Optional filter for attribute types to include.</param>
 405    /// <param name="componentNameArgument">The argument name to use for component names.</param>
 406    /// <param name="pending">The pending standalone attribute list (cleared on match).</param>
 407    /// <returns><c>true</c> if the statement was handled; otherwise <c>false</c>.</returns>
 408    private static bool TryHandleInlineAttributedAssignment(
 409        StatementAst statement,
 410        Dictionary<string, AnnotatedVariable> variables,
 411        IReadOnlyCollection<string>? attributeTypeFilter,
 412        string componentNameArgument,
 413        List<AttributeAst> pending)
 414    {
 5415        if (statement is not AssignmentStatementAst inlineAssign)
 416        {
 2417            return false;
 418        }
 419
 3420        if (!TryExtractInlineAttributedAssignment(inlineAssign, attributeTypeFilter, out var inlineVarName, out var inli
 421        {
 0422            return false;
 423        }
 424
 3425        var (initValue, initExpr) = EvaluateValueStatement(inlineAssign.Right);
 3426        var entry = GetOrCreateVariable(variables, inlineVarName);
 3427        entry.VariableType ??= inlineVarType;
 3428        entry.VariableTypeName ??= inlineVarTypeName;
 429
 3430        if (initExpr != "NoDefault")
 431        {
 1432            entry.NoDefault = false;
 1433            entry.InitialValue ??= initValue;
 1434            entry.InitialValueExpression ??= initExpr;
 435        }
 436        else
 437        {
 2438            entry.NoDefault = true;
 439        }
 440
 12441        foreach (var a in inlineAttrs)
 442        {
 3443            var ka = TryCreateAnnotation(a, defaultComponentName: inlineVarName, componentNameArgument);
 3444            if (ka is not null)
 445            {
 3446                entry.Annotations.Add(ka);
 447            }
 448        }
 449
 3450        pending.Clear();
 3451        return true;
 452    }
 453
 454    /// <summary>
 455    /// Handles inline attributed declarations, e.g. <c>[Attr()][int]$x</c>, attaching annotations.
 456    /// </summary>
 457    /// <param name="statement">The statement to inspect.</param>
 458    /// <param name="variables">The variable map to populate.</param>
 459    /// <param name="attributeTypeFilter">Optional filter for attribute types to include.</param>
 460    /// <param name="componentNameArgument">The argument name to use for component names.</param>
 461    /// <param name="pending">The pending standalone attribute list (cleared on match).</param>
 462    /// <returns><c>true</c> if the statement was handled; otherwise <c>false</c>.</returns>
 463    private static bool TryHandleInlineAttributedDeclaration(
 464        StatementAst statement,
 465        Dictionary<string, AnnotatedVariable> variables,
 466        IReadOnlyCollection<string>? attributeTypeFilter,
 467        string componentNameArgument,
 468        List<AttributeAst> pending)
 469    {
 2470        if (!TryExtractInlineAttributedDeclaration(statement, attributeTypeFilter, out var varName, out var varType, out
 471        {
 1472            return false;
 473        }
 474
 1475        var entry = GetOrCreateVariable(variables, varName);
 1476        entry.VariableType ??= varType;
 1477        entry.VariableTypeName ??= varTypeName;
 478
 4479        foreach (var a in attrs)
 480        {
 1481            var ka = TryCreateAnnotation(a, defaultComponentName: varName, componentNameArgument);
 1482            if (ka is not null)
 483            {
 1484                entry.Annotations.Add(ka);
 485            }
 486        }
 487
 1488        pending.Clear();
 1489        return true;
 490    }
 491
 492    /// <summary>
 493    /// Handles standalone attribute lines by accumulating matching attributes into <paramref name="pending"/>.
 494    /// </summary>
 495    /// <param name="statement">The statement to inspect.</param>
 496    /// <param name="attributeTypeFilter">Optional filter for attribute types to include.</param>
 497    /// <param name="pending">The pending standalone attribute list to append to.</param>
 498    /// <returns><c>true</c> if the statement was an attribute line; otherwise <c>false</c>.</returns>
 499    private static bool TryHandleStandaloneAttributeLine(
 500        StatementAst statement,
 501        IReadOnlyCollection<string>? attributeTypeFilter,
 502        List<AttributeAst> pending)
 503    {
 1504        var parsedAttrs = TryParseStandaloneAttributeLine(statement.Extent.Text);
 1505        if (parsedAttrs.Count == 0)
 506        {
 1507            return false;
 508        }
 509
 0510        foreach (var a in parsedAttrs)
 511        {
 0512            if (IsMatchingAttribute(a, attributeTypeFilter))
 513            {
 0514                pending.Add(a);
 515            }
 516        }
 517
 0518        return true;
 519    }
 520
 521    /// <summary>
 522    /// Handles variable assignments, attaching pending standalone attributes and capturing type/initializer.
 523    /// </summary>
 524    /// <param name="statement">The statement to inspect.</param>
 525    /// <param name="variables">The variable map to populate.</param>
 526    /// <param name="componentNameArgument">The argument name to use for component names.</param>
 527    /// <param name="pending">The pending standalone attribute list (cleared when applied).</param>
 528    /// <returns><c>true</c> if the statement was handled; otherwise <c>false</c>.</returns>
 529    private static bool TryHandleVariableAssignment(
 530        StatementAst statement,
 531        Dictionary<string, AnnotatedVariable> variables,
 532        string componentNameArgument,
 533        List<AttributeAst> pending)
 534    {
 1535        if (statement is not AssignmentStatementAst assign)
 536        {
 1537            return false;
 538        }
 539
 0540        if (!TryGetAssignmentTarget(assign.Left, out var targetVarName, out var targetVarType, out var targetVarTypeName
 541        {
 0542            return false;
 543        }
 544
 0545        var shouldCapture = pending.Count > 0 || variables.ContainsKey(targetVarName);
 0546        if (!shouldCapture)
 547        {
 0548            return false;
 549        }
 550
 0551        var (initValue, initExpr) = EvaluateValueStatement(assign.Right);
 0552        var entry = GetOrCreateVariable(variables, targetVarName);
 553
 0554        ApplyVariableTypeInfo(entry, targetVarType, targetVarTypeName);
 0555        ApplyInitializerValues(entry, initValue, initExpr);
 0556        ApplyPendingAnnotations(entry, pending, targetVarName, componentNameArgument);
 557
 0558        pending.Clear();
 0559        return true;
 560    }
 561
 562    /// <summary>
 563    /// Applies variable type information to an AnnotatedVariable entry.
 564    /// </summary>
 565    /// <param name="entry">The entry to modify.</param>
 566    /// <param name="variableType">The variable type to apply.</param>
 567    /// <param name="variableTypeName">The variable type name to apply.</param>
 568    private static void ApplyVariableTypeInfo(AnnotatedVariable entry, Type? variableType, string? variableTypeName)
 569    {
 0570        entry.VariableType ??= variableType;
 0571        entry.VariableTypeName ??= variableTypeName;
 0572    }
 573
 574    /// <summary>
 575    /// Applies initializer values to an AnnotatedVariable entry, handling default/no-default cases.
 576    /// </summary>
 577    /// <param name="entry">The entry to modify.</param>
 578    /// <param name="initValue">The initial value to apply.</param>
 579    /// <param name="initExpr">The initial value expression to apply.</param>
 580    private static void ApplyInitializerValues(AnnotatedVariable entry, object? initValue, string? initExpr)
 581    {
 0582        if (initExpr == "NoDefault")
 583        {
 0584            entry.NoDefault = true;
 0585            return;
 586        }
 587
 0588        entry.NoDefault = false;
 0589        entry.InitialValue ??= initValue;
 0590        entry.InitialValueExpression ??= initExpr;
 0591    }
 592
 593    /// <summary>
 594    /// Applies pending standalone attributes to an AnnotatedVariable entry.
 595    /// </summary>
 596    /// <param name="entry">The entry to modify.</param>
 597    /// <param name="pending">The pending attributes to apply.</param>
 598    /// <param name="targetVarName">The variable name used as default component name.</param>
 599    /// <param name="componentNameArgument">The argument name to use for component names.</param>
 600    private static void ApplyPendingAnnotations(
 601        AnnotatedVariable entry,
 602        List<AttributeAst> pending,
 603        string targetVarName,
 604        string componentNameArgument)
 605    {
 0606        if (pending.Count == 0)
 607        {
 0608            return;
 609        }
 610
 0611        foreach (var a in pending)
 612        {
 0613            var ka = TryCreateAnnotation(a, defaultComponentName: targetVarName, componentNameArgument);
 0614            if (ka is not null)
 615            {
 0616                entry.Annotations.Add(ka);
 617            }
 618        }
 0619    }
 620
 621    /// <summary>
 622    /// Handles declaration-only variables (no assignment), applying any pending standalone attributes.
 623    /// </summary>
 624    /// <param name="statement">The statement to inspect.</param>
 625    /// <param name="variables">The variable map to populate.</param>
 626    /// <param name="componentNameArgument">The argument name to use for component names.</param>
 627    /// <param name="pending">The pending standalone attribute list (cleared when applied).</param>
 628    /// <returns><c>true</c> if the statement was handled; otherwise <c>false</c>.</returns>
 629    private static bool TryHandleDeclarationOnlyVariable(
 630        StatementAst statement,
 631        Dictionary<string, AnnotatedVariable> variables,
 632        string componentNameArgument,
 633        List<AttributeAst> pending)
 634    {
 1635        if (pending.Count == 0)
 636        {
 1637            return false;
 638        }
 639
 0640        if (statement is not CommandExpressionAst declExpr)
 641        {
 0642            return false;
 643        }
 644
 0645        if (!TryGetDeclaredVariableInfo(declExpr.Expression, out var declaredVarName, out var declaredVarType, out var d
 646        {
 0647            return false;
 648        }
 649
 0650        var entry = GetOrCreateVariable(variables, declaredVarName);
 0651        entry.VariableType ??= declaredVarType;
 0652        entry.VariableTypeName ??= declaredVarTypeName;
 653
 0654        foreach (var a in pending)
 655        {
 0656            var ka = TryCreateAnnotation(a, defaultComponentName: declaredVarName, componentNameArgument);
 0657            if (ka is not null)
 658            {
 0659                entry.Annotations.Add(ka);
 660            }
 661        }
 662
 0663        pending.Clear();
 0664        return true;
 665    }
 666
 667    /// <summary>
 668    /// Gets or creates an AnnotatedVariable entry in the dictionary.
 669    /// </summary>
 670    /// <param name="variables"> The dictionary of variables to search or add to.</param>
 671    /// <param name="varName"> The name of the variable to get or create.</param>
 672    /// <returns>The existing or newly created AnnotatedVariable entry.</returns>
 673    private static AnnotatedVariable GetOrCreateVariable(Dictionary<string, AnnotatedVariable> variables, string varName
 674    {
 4675        if (!variables.TryGetValue(varName, out var entry))
 676        {
 4677            entry = new AnnotatedVariable(varName);
 4678            variables[varName] = entry;
 679        }
 680
 4681        return entry;
 682    }
 683
 684    /// <summary>
 685    /// Tries to extract inline attributed variable declaration information from a statement AST.
 686    /// </summary>
 687    /// <param name="statement">The statement AST to inspect.</param>
 688    /// <param name="attributeTypeFilter">Optional filter for attribute types to include.</param>
 689    /// <param name="variableName">Output variable name if found.</param>
 690    /// <param name="variableType">Output variable type if declared.</param>
 691    /// <param name="variableTypeName">Output variable type name as written in script if declared.</param>
 692    /// <param name="attributes">Output list of matching attributes found.</param>
 693    /// <returns><c>true</c> if matching attributes were found; otherwise <c>false</c>.</returns>
 694    private static bool TryExtractInlineAttributedDeclaration(
 695        StatementAst statement,
 696        IReadOnlyCollection<string>? attributeTypeFilter,
 697        out string variableName,
 698        out Type? variableType,
 699        out string? variableTypeName,
 700        out IReadOnlyList<AttributeAst> attributes)
 701    {
 2702        variableName = string.Empty;
 2703        variableType = null;
 2704        variableTypeName = null;
 2705        attributes = [];
 706
 2707        if (!TryExtractExpressionFromStatement(statement, out var expr))
 708        {
 1709            return false;
 710        }
 711
 712        // Check for attributed-expression chain
 1713        if (expr is null)
 714        {
 0715            return false;
 716        }
 717
 1718        var found = CollectMatchingAttributesFromChain(expr, attributeTypeFilter);
 1719        if (found.Count == 0)
 720        {
 0721            return false;
 722        }
 723
 1724        if (!TryGetDeclaredVariableInfo(expr, out variableName, out variableType, out variableTypeName))
 725        {
 0726            return false;
 727        }
 728
 1729        attributes = found;
 1730        return true;
 731    }
 732
 733    /// <summary>
 734    /// Tries to extract an expression from a statement AST, handling CommandExpression and Pipeline variants.
 735    /// </summary>
 736    /// <param name="statement">The statement AST to extract from.</param>
 737    /// <param name="expr">The extracted expression if found.</param>
 738    /// <returns><c>true</c> if an expression was successfully extracted; otherwise <c>false</c>.</returns>
 739    private static bool TryExtractExpressionFromStatement(StatementAst statement, out ExpressionAst? expr)
 740    {
 2741        expr = statement switch
 2742        {
 0743            CommandExpressionAst ce => ce.Expression,
 3744            PipelineAst p when p.PipelineElements is { Count: 1 } && p.PipelineElements[0] is CommandExpressionAst ce =>
 1745            _ => null
 2746        };
 747
 2748        return expr is not null;
 749    }
 750
 751    /// <summary>
 752    /// Collects matching attributes from an attributed-expression chain.
 753    /// </summary>
 754    /// <param name="expr">The expression to traverse.</param>
 755    /// <param name="attributeTypeFilter">Optional filter for attribute types to include.</param>
 756    /// <returns>A list of matching attributes found in the chain.</returns>
 757    private static List<AttributeAst> CollectMatchingAttributesFromChain(ExpressionAst expr, IReadOnlyCollection<string>
 758    {
 1759        var found = new List<AttributeAst>();
 1760        var cursor = expr;
 761
 3762        while (cursor is AttributedExpressionAst aex)
 763        {
 2764            if (aex.Attribute is AttributeAst attr && IsMatchingAttribute(attr, attributeTypeFilter))
 765            {
 1766                found.Add(attr);
 767            }
 2768            cursor = aex.Child;
 2769        }
 770
 1771        return found;
 772    }
 773
 774    /// <summary>
 775    /// Tries to extract inline attributed variable assignment information from an assignment AST.
 776    /// </summary>
 777    /// <param name="assignment">The assignment statement AST to inspect.</param>
 778    /// <param name="attributeTypeFilter">Optional filter for attribute types to include.</param>
 779    /// <param name="variableName">Output variable name if found.</param>
 780    /// <param name="variableType">Output variable type if declared.</param>
 781    /// <param name="variableTypeName">Output variable type name as written in script if declared.</param>
 782    /// <param name="attributes">Output list of matching attributes found.</param>
 783    /// <returns><c>true</c> if matching attributes were found; otherwise <c>false</c>.</returns>
 784    private static bool TryExtractInlineAttributedAssignment(
 785        AssignmentStatementAst assignment,
 786        IReadOnlyCollection<string>? attributeTypeFilter,
 787        out string variableName,
 788        out Type? variableType,
 789        out string? variableTypeName,
 790        out IReadOnlyList<AttributeAst> attributes)
 791    {
 3792        variableName = string.Empty;
 3793        variableType = null;
 3794        variableTypeName = null;
 3795        attributes = [];
 796
 797        // Collect matching attributes from the left-hand attributed-expression chain.
 3798        var found = new List<AttributeAst>();
 3799        var cursor = assignment.Left;
 9800        while (cursor is AttributedExpressionAst aex)
 801        {
 6802            if (aex.Attribute is AttributeAst attr && IsMatchingAttribute(attr, attributeTypeFilter))
 803            {
 3804                found.Add(attr);
 805            }
 6806            cursor = aex.Child;
 6807        }
 808
 3809        if (found.Count == 0)
 810        {
 0811            return false;
 812        }
 813
 3814        if (!TryGetDeclaredVariableInfo(assignment.Left, out variableName, out variableType, out variableTypeName))
 815        {
 0816            return false;
 817        }
 818
 3819        attributes = found;
 3820        return true;
 821    }
 822
 823    /// <summary>
 824    ///     Tries to extract the assignment target variable information from the left-hand side of an assignment.
 825    /// </summary>
 826    /// <param name="left">The left-hand side expression of the assignment.</param>
 827    /// <param name="variableName">Output variable name if found.</param>
 828    /// <param name="variableType">Output variable type if declared.</param>
 829    /// <param name="variableTypeName">Output variable type name as written in script if declared.</param>
 830    /// <returns><c>true</c> if a variable declaration was found; otherwise <c>false</c>.</returns>
 831    /// <remarks>
 832    /// Assignment can be: $x = 1   or   [int]$x = 1
 833    /// </remarks>
 834    private static bool TryGetAssignmentTarget(ExpressionAst left, out string variableName, out Type? variableType, out 
 0835        => TryGetDeclaredVariableInfo(left, out variableName, out variableType, out variableTypeName);
 836
 837    /// <summary>
 838    /// Tries to extract declared variable information from an expression AST.
 839    /// </summary>
 840    /// <param name="expr">The expression AST to inspect.</param>
 841    /// <param name="variableName">Output variable name if found.</param>
 842    /// <param name="variableType">Output variable type if declared.</param>
 843    /// <param name="variableTypeName">Output variable type name as written in script if declared.</param>
 844    /// <returns><c>true</c> if a variable declaration was found; otherwise <c>false</c>.</returns>
 845    private static bool TryGetDeclaredVariableInfo(ExpressionAst expr, out string variableName, out Type? variableType, 
 846    {
 4847        variableName = string.Empty;
 4848        var cursor = expr;
 849
 4850        var attributedTypeNames = UnwrapAttributedExpressionChain(ref cursor);
 4851        _ = TryUnwrapConvertExpression(ref cursor, out variableType, out variableTypeName);
 4852        UnwrapRemainingAttributedExpressions(ref cursor);
 853
 4854        if (cursor is not VariableExpressionAst v)
 855        {
 0856            return false;
 857        }
 858
 4859        variableName = v.VariablePath.UserPath;
 860
 861        // PowerShell sometimes represents type constraints like [Nullable[datetime]]$x as another
 862        // attributed-expression rather than a ConvertExpressionAst. If we didn't get a type above,
 863        // try to infer it from the last non-annotation attribute type name.
 4864        if (variableType is null && variableTypeName is null && attributedTypeNames.Count > 0 &&
 4865            TryInferVariableTypeFromAttributes(attributedTypeNames, out var inferredType, out var inferredTypeName))
 866        {
 4867            variableType = inferredType;
 4868            variableTypeName = inferredTypeName;
 869        }
 870
 4871        return !string.IsNullOrWhiteSpace(variableName);
 872    }
 873
 874    /// <summary>
 875    /// Unwraps an attributed-expression chain and collects any attribute type names encountered.
 876    /// </summary>
 877    /// <param name="cursor">The current expression cursor (updated to the innermost child).</param>
 878    /// <returns>A list of attribute type names encountered while unwrapping.</returns>
 879    private static List<string> UnwrapAttributedExpressionChain(ref ExpressionAst cursor)
 880    {
 4881        var attributedTypeNames = new List<string>();
 882
 12883        while (cursor is AttributedExpressionAst aex)
 884        {
 8885            if (!string.IsNullOrWhiteSpace(aex.Attribute?.TypeName?.FullName))
 886            {
 8887                attributedTypeNames.Add(aex.Attribute.TypeName.FullName);
 888            }
 889
 8890            cursor = aex.Child;
 8891        }
 892
 4893        return attributedTypeNames;
 894    }
 895
 896    /// <summary>
 897    /// Unwraps a type conversion expression (e.g. <c>[int]$x</c>) when present and resolves the .NET type.
 898    /// </summary>
 899    /// <param name="cursor">The current expression cursor (updated to the conversion child when unwrapped).</param>
 900    /// <param name="variableType">The resolved .NET type for the conversion, if any.</param>
 901    /// <param name="variableTypeName">The declared type name as written in script, if any.</param>
 902    /// <returns><c>true</c> if a conversion expression was unwrapped; otherwise <c>false</c>.</returns>
 903    private static bool TryUnwrapConvertExpression(ref ExpressionAst cursor, out Type? variableType, out string? variabl
 904    {
 4905        if (cursor is ConvertExpressionAst cex)
 906        {
 0907            variableTypeName = cex.Type.TypeName.FullName;
 0908            variableType = ResolvePowerShellTypeName(variableTypeName);
 0909            cursor = cex.Child;
 0910            return true;
 911        }
 912
 4913        variableType = null;
 4914        variableTypeName = null;
 4915        return false;
 916    }
 917
 918    /// <summary>
 919    /// Unwraps any remaining attributed expressions (best-effort) after other unwrapping steps.
 920    /// </summary>
 921    /// <param name="cursor">The current expression cursor (updated to the innermost child).</param>
 922    private static void UnwrapRemainingAttributedExpressions(ref ExpressionAst cursor)
 923    {
 4924        while (cursor is AttributedExpressionAst aex)
 925        {
 0926            cursor = aex.Child;
 0927        }
 4928    }
 929
 930    /// <summary>
 931    /// Tries to infer a declared variable type from the collected attribute type names, ignoring annotation attributes.
 932    /// </summary>
 933    /// <param name="attributedTypeNames">Attribute type names collected while unwrapping.</param>
 934    /// <param name="variableType">The inferred .NET type.</param>
 935    /// <param name="variableTypeName">The inferred type name as written in script.</param>
 936    /// <returns><c>true</c> if a non-annotation type was inferred; otherwise <c>false</c>.</returns>
 937    private static bool TryInferVariableTypeFromAttributes(IReadOnlyList<string> attributedTypeNames, out Type? variable
 938    {
 8939        for (var i = attributedTypeNames.Count - 1; i >= 0; i--)
 940        {
 4941            var tn = attributedTypeNames[i];
 4942            var t = ResolvePowerShellTypeName(tn);
 4943            if (t is null)
 944            {
 945                continue;
 946            }
 947
 4948            if (typeof(KestrunAnnotation).IsAssignableFrom(t))
 949            {
 950                continue;
 951            }
 952
 4953            variableType = t;
 4954            variableTypeName = tn;
 4955            return true;
 956        }
 957
 0958        variableType = null;
 0959        variableTypeName = null;
 0960        return false;
 961    }
 962
 963    /// <summary>
 964    /// Resolves a PowerShell type name to a .NET Type, handling common accelerators and syntax.
 965    /// </summary>
 966    /// <param name="name">The PowerShell type name to resolve.</param>
 967    /// <returns>The resolved .NET Type, or null if resolution failed.</returns>
 968    private static Type? ResolvePowerShellTypeName(string? name)
 969    {
 4970        if (string.IsNullOrWhiteSpace(name))
 971        {
 0972            return null;
 973        }
 974
 975        // Best-effort resolution via PowerShell's own type name resolver.
 976        // This handles common accelerators and PowerShell syntax like Nullable[datetime].
 977        try
 978        {
 4979            var psType = new PSTypeName(name.Trim()).Type;
 4980            if (psType is not null)
 981            {
 4982                return psType;
 983            }
 0984        }
 0985        catch
 986        {
 987            // Ignore and fall back to our heuristic mapping.
 0988        }
 989
 990        // Common accelerators
 0991        var lowered = name.Trim();
 0992        return lowered.ToLowerInvariant() switch
 0993        {
 0994            "int" => typeof(int),
 0995            "long" => typeof(long),
 0996            "double" => typeof(double),
 0997            "float" => typeof(float),
 0998            "decimal" => typeof(decimal),
 0999            "bool" => typeof(bool),
 01000            "string" => typeof(string),
 01001            "datetime" => typeof(DateTime),
 01002            "guid" => typeof(Guid),
 01003            "ipaddress" => typeof(System.Net.IPAddress),
 01004            "hashtable" => typeof(Hashtable),
 01005            "object" => typeof(object),
 01006            _ => ResolveTypeFromName(NormalizePowerShellTypeName(lowered))
 01007        };
 41008    }
 1009
 1010    private static string NormalizePowerShellTypeName(string name)
 1011    {
 1012        // Handle Nullable[T]
 01013        if (name.StartsWith("nullable[", StringComparison.OrdinalIgnoreCase) && name.EndsWith(']'))
 1014        {
 01015            var inner = name[9..^1];
 01016            var innerType = ResolvePowerShellTypeName(inner);
 01017            if (innerType is not null && innerType.IsValueType)
 1018            {
 01019                return typeof(Nullable<>).MakeGenericType(innerType).FullName!;
 1020            }
 1021        }
 1022
 1023        // Handle array syntax: datetime[]
 01024        if (name.EndsWith("[]", StringComparison.Ordinal))
 1025        {
 01026            var inner = name[..^2];
 01027            var innerType = ResolvePowerShellTypeName(inner);
 01028            if (innerType is not null)
 1029            {
 01030                return innerType.MakeArrayType().FullName!;
 1031            }
 1032        }
 1033
 01034        return name;
 1035    }
 1036
 1037    private static (object? Value, string? Expression) EvaluateValueStatement(StatementAst statement)
 1038    {
 1039        // RHS of assignment is a StatementAst. Try to extract a single expression from it.
 31040        var expr = statement switch
 31041        {
 11042            CommandExpressionAst ce => ce.Expression,
 21043            PipelineAst p when p.PipelineElements is { Count: 1 } && p.PipelineElements[0] is CommandExpressionAst ce =>
 21044            _ => null
 31045        };
 1046
 31047        if (expr is null)
 1048        {
 21049            var raw = statement.Extent.Text.Trim();
 21050            return (string.IsNullOrWhiteSpace(raw) ? null : raw, string.IsNullOrWhiteSpace(raw) ? null : raw);
 1051        }
 1052
 11053        var value = EvaluateArgumentExpression(expr);
 11054        var text = expr.Extent.Text.Trim();
 11055        return (value, string.IsNullOrWhiteSpace(text) ? null : text);
 1056    }
 1057
 1058    private static KestrunAnnotation? TryCreateAnnotation(
 1059        AttributeAst attr,
 1060        string defaultComponentName,
 1061        string componentNameArgument)
 1062    {
 41063        var attribute = TryCreateKestrunAnnotation(attr, defaultComponentName, componentNameArgument);
 41064        if (attribute is not null)
 1065        {
 41066            return attribute;
 1067        }
 1068
 01069        attribute = TryCmdletMetadataAttribute(attr);
 1070
 01071        return attribute;
 1072    }
 1073    private static KestrunAnnotation? TryCreateKestrunAnnotation(
 1074        AttributeAst attr,
 1075        string defaultComponentName,
 1076        string componentNameArgument)
 1077    {
 41078        var annotationType = ResolveKestrunAnnotationType(attr);
 41079        if (annotationType is null)
 1080        {
 01081            return null;
 1082        }
 41083        var activatedInstance = Activator.CreateInstance(annotationType);
 1084
 41085        if (activatedInstance is not KestrunAnnotation instance)
 1086        {
 01087            return null;
 1088        }
 41089        if (activatedInstance is OpenApiExtensionAttribute)
 1090        {
 01091            var par1 = attr.PositionalArguments.ElementAtOrDefault(0)?.Extent.Text.Trim('\'');
 01092            var par2 = attr.PositionalArguments.ElementAtOrDefault(1)?.Extent.Text.Trim('\'');
 01093            if (par1 is null || par2 is null)
 1094            {
 01095                return null;
 1096            }
 1097
 01098            instance = new OpenApiExtensionAttribute(par1, par2);
 1099        }
 1100        else
 1101        {
 1102            // Apply named arguments as property setters.
 241103            foreach (var na in attr.NamedArguments ?? Enumerable.Empty<NamedAttributeArgumentAst>())
 1104            {
 81105                ApplyNamedArgument(instance, na);
 1106            }
 1107        }
 1108        // If the annotation supports a component-name argument, apply a default when not specified.
 1109        // This preserves the previous behavior where the variable name becomes the component key.
 41110        ApplyDefaultComponentName(instance, defaultComponentName, componentNameArgument);
 1111
 41112        return instance;
 1113    }
 1114
 1115    /// <summary>
 1116    /// Tries to evaluate an expression AST to a constant-like value.
 1117    /// </summary>
 1118    /// <param name="expr"> The expression AST to evaluate. </param>
 1119    /// <returns>The constant-like value if evaluation is successful; otherwise, null.</returns>
 1120    private static object? TryGetConstantLikeValue(ExpressionAst? expr)
 1121    {
 01122        if (expr is null)
 1123        {
 01124            return null;
 1125        }
 1126
 01127        if (TryGetPlainConstantValue(expr, out var value))
 1128        {
 01129            return value;
 1130        }
 1131
 01132        if (TryGetPlainStringValue(expr, out value))
 1133        {
 01134            return value;
 1135        }
 1136
 01137        if (TryGetStaticTypeMemberValue(expr, out value))
 1138        {
 01139            return value;
 1140        }
 1141        // Could extend with more expression types as needed.
 01142        return null;
 1143    }
 1144
 1145    /// <summary>
 1146    /// Tries to extract a plain constant value from an expression, such as numbers and booleans.
 1147    /// </summary>
 1148    /// <param name="expr">The expression to evaluate.</param>
 1149    /// <param name="value">The extracted constant value.</param>
 1150    /// <returns><c>true</c> if a constant value was extracted; otherwise <c>false</c>.</returns>
 1151    private static bool TryGetPlainConstantValue(ExpressionAst expr, out object? value)
 1152    {
 01153        if (expr is ConstantExpressionAst c)
 1154        {
 01155            value = c.Value;
 01156            return true;
 1157        }
 1158
 01159        value = null;
 01160        return false;
 1161    }
 1162
 1163    /// <summary>
 1164    /// Tries to extract a plain string literal value from an expression.
 1165    /// </summary>
 1166    /// <param name="expr">The expression to evaluate.</param>
 1167    /// <param name="value">The extracted string value.</param>
 1168    /// <returns><c>true</c> if a string literal value was extracted; otherwise <c>false</c>.</returns>
 1169    private static bool TryGetPlainStringValue(ExpressionAst expr, out object? value)
 1170    {
 01171        if (expr is StringConstantExpressionAst s)
 1172        {
 01173            value = s.Value;
 01174            return true;
 1175        }
 1176
 01177        value = null;
 01178        return false;
 1179    }
 1180
 1181    /// <summary>
 1182    /// Tries to evaluate a static type member expression (e.g. <c>[int]::MaxValue</c>) to its runtime value.
 1183    /// </summary>
 1184    /// <param name="expr">The expression to evaluate.</param>
 1185    /// <param name="value">The evaluated member value.</param>
 1186    /// <returns><c>true</c> if the expression was a supported static member and was evaluated; otherwise <c>false</c>.<
 1187    private static bool TryGetStaticTypeMemberValue(ExpressionAst expr, out object? value)
 1188    {
 01189        value = null;
 1190
 01191        if (expr is not MemberExpressionAst me || !me.Static || me.Expression is not TypeExpressionAst te)
 1192        {
 01193            return false;
 1194        }
 1195
 01196        var targetType = te.TypeName.GetReflectionType(); // resolves [int] to System.Int32, etc.
 01197        if (targetType is null)
 1198        {
 01199            return false;
 1200        }
 1201
 01202        var memberName = GetMemberName(me.Member);
 01203        if (string.IsNullOrWhiteSpace(memberName))
 1204        {
 01205            return false;
 1206        }
 1207
 01208        if (TryGetReflectedPropertyValue(targetType, memberName, out value))
 1209        {
 01210            return true;
 1211        }
 1212
 01213        if (TryGetReflectedFieldValue(targetType, memberName, out value))
 1214        {
 01215            return true;
 1216        }
 1217
 1218        // Unsupported member type
 01219        return false;
 1220    }
 1221
 1222    /// <summary>
 1223    /// Tries to retrieve and invoke a public static property from a type.
 1224    /// </summary>
 1225    /// <param name="targetType">The type to inspect.</param>
 1226    /// <param name="memberName">The property name to retrieve.</param>
 1227    /// <param name="value">The property value if found and invoked.</param>
 1228    /// <returns><c>true</c> if the property was found and its value retrieved; otherwise <c>false</c>.</returns>
 1229    private static bool TryGetReflectedPropertyValue(Type targetType, string memberName, out object? value)
 1230    {
 1231        const BindingFlags flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy;
 01232        var prop = targetType.GetProperty(memberName, flags);
 1233
 01234        if (prop is not null)
 1235        {
 01236            value = prop.GetValue(null);
 01237            return true;
 1238        }
 1239
 01240        value = null;
 01241        return false;
 1242    }
 1243
 1244    /// <summary>
 1245    /// Tries to retrieve and invoke a public static field from a type.
 1246    /// </summary>
 1247    /// <param name="targetType">The type to inspect.</param>
 1248    /// <param name="memberName">The field name to retrieve.</param>
 1249    /// <param name="value">The field value if found and invoked.</param>
 1250    /// <returns><c>true</c> if the field was found and its value retrieved; otherwise <c>false</c>.</returns>
 1251    private static bool TryGetReflectedFieldValue(Type targetType, string memberName, out object? value)
 1252    {
 1253        const BindingFlags flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy;
 01254        var field = targetType.GetField(memberName, flags);
 1255
 01256        if (field is not null)
 1257        {
 01258            value = field.GetValue(null);
 01259            return true;
 1260        }
 1261
 01262        value = null;
 01263        return false;
 1264    }
 1265
 1266    /// <summary>
 1267    /// Tries to extract a member name from a PowerShell member expression member node.
 1268    /// </summary>
 1269    /// <param name="member">The member expression node.</param>
 1270    /// <returns>The member name if it can be extracted; otherwise, <c>null</c>.</returns>
 1271    private static string? GetMemberName(CommandElementAst member)
 01272        => (member as StringConstantExpressionAst)?.Value
 01273           ?? (member as ConstantExpressionAst)?.Value?.ToString();
 1274
 1275    private static KestrunAnnotation? TryCmdletMetadataAttribute(
 1276            AttributeAst attr)
 1277    {
 01278        var annotationType = ResolveCmdletMetadataAttributeType(attr);
 01279        if (annotationType is null)
 1280        {
 01281            return null;
 1282        }
 1283
 01284        var instance = new InternalPowershellAttribute();
 01285        switch (annotationType.Name) // <-- no GetType().Name
 1286        {
 1287            case nameof(ValidateRangeAttribute):
 1288                {
 01289                    var minObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(0));
 01290                    var maxObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(1));
 1291
 01292                    instance.MinRange = minObj?.ToString();
 01293                    instance.MaxRange = maxObj?.ToString();
 01294                    break;
 1295                }
 1296
 1297            case nameof(ValidateLengthAttribute):
 1298                {
 01299                    var minObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(0));
 01300                    var maxObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(1));
 01301                    if (int.TryParse(minObj?.ToString(), out var minLength))
 1302                    {
 01303                        instance.MinLength = minLength;
 1304                    }
 1305
 01306                    if (int.TryParse(maxObj?.ToString(), out var maxLength))
 1307                    {
 01308                        instance.MaxLength = maxLength;
 1309                    }
 1310
 01311                    break;
 1312                }
 1313            case nameof(ValidateSetAttribute):
 1314                {
 1315                    // PowerShell: [ValidateSet('a','b')] is positional arguments
 01316                    instance.AllowedValues = [.. attr.PositionalArguments
 01317                    .Select(a => TryGetConstantLikeValue(a)?.ToString() ?? string.Empty)
 01318                    .Where(s => !string.IsNullOrEmpty(s))];
 01319                    break;
 1320                }
 1321
 1322            case nameof(ValidatePatternAttribute):
 1323                {
 1324                    // PowerShell: [ValidatePattern('regex')]
 01325                    var patternObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(0));
 01326                    instance.RegexPattern = patternObj?.ToString();
 01327                    break;
 1328                }
 1329
 1330            case nameof(ValidateCountAttribute):
 1331                {
 1332                    // PowerShell: [ValidateCount(min, max)]
 01333                    var minObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(0));
 01334                    var maxObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(1));
 1335
 01336                    if (int.TryParse(minObj?.ToString(), out var minCount))
 1337                    {
 01338                        instance.MinItems = minCount;
 1339                    }
 1340
 01341                    if (int.TryParse(maxObj?.ToString(), out var maxCount))
 1342                    {
 01343                        instance.MaxItems = maxCount;
 1344                    }
 1345
 01346                    break;
 1347                }
 1348
 1349            case nameof(ValidateNotNullOrEmptyAttribute):
 01350                instance.ValidateNotNullOrEmptyAttribute = true;
 01351                break;
 1352
 1353            case nameof(ValidateNotNullAttribute):
 01354                instance.ValidateNotNullAttribute = true;
 01355                break;
 1356
 1357            case nameof(ValidateNotNullOrWhiteSpaceAttribute):
 01358                instance.ValidateNotNullOrWhiteSpaceAttribute = true;
 1359                break;
 1360        }
 1361
 01362        return instance;
 1363    }
 1364    private static void ApplyDefaultComponentName(Attribute annotation, string defaultComponentName, string componentNam
 1365    {
 41366        if (string.IsNullOrWhiteSpace(defaultComponentName))
 1367        {
 01368            return;
 1369        }
 1370
 41371        var t = annotation.GetType();
 41372        var prop = t.GetProperty(componentNameArgument, BindingFlags.Instance | BindingFlags.Public | BindingFlags.Ignor
 41373        if (prop is null || prop.PropertyType != typeof(string) || !prop.CanWrite)
 1374        {
 41375            return;
 1376        }
 1377
 01378        var current = prop.GetValue(annotation) as string;
 01379        if (!string.IsNullOrWhiteSpace(current))
 1380        {
 01381            return;
 1382        }
 1383
 01384        prop.SetValue(annotation, defaultComponentName);
 01385    }
 1386
 1387    private static void ApplyNamedArgument(KestrunAnnotation annotation, NamedAttributeArgumentAst na)
 1388    {
 81389        var t = annotation.GetType();
 81390        var prop = t.GetProperty(na.ArgumentName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase)
 81391        if (prop is null || !prop.CanWrite)
 1392        {
 01393            return;
 1394        }
 1395
 81396        var raw = EvaluateArgumentExpression(na.Argument);
 81397        var converted = ConvertToPropertyType(raw, prop.PropertyType);
 1398        try
 1399        {
 1400            // Avoid failing configuration due to a best-effort scan conversion.
 81401            if (converted is not null)
 1402            {
 81403                var targetType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
 81404                if (!targetType.IsInstanceOfType(converted) && !targetType.IsAssignableFrom(converted.GetType()))
 1405                {
 01406                    return;
 1407                }
 1408            }
 1409
 81410            prop.SetValue(annotation, converted);
 81411        }
 01412        catch (Exception ex)
 1413        {
 1414            // Log and continue on invalid property sets during best-effort scan.
 01415            Serilog.Log.Warning(
 01416                ex,
 01417                "Failed to set property '{PropertyName}' on annotation '{AnnotationType}' with value expression '{ValueE
 01418                na.ArgumentName,
 01419                annotation.GetType().Name,
 01420                na.Argument);
 01421        }
 81422    }
 1423
 1424    /// <summary>
 1425    /// Evaluates an argument expression AST to a runtime value.
 1426    /// </summary>
 1427    /// <param name="expr"> The expression AST to evaluate</param>
 1428    /// <returns>The evaluated runtime value</returns>
 121429    private static object? EvaluateArgumentExpression(ExpressionAst expr) => expr switch
 121430    {
 11431        ArrayLiteralAst a => a.Elements.Select(EvaluateArgumentExpression).ToArray(),
 11432        ParenExpressionAst p => EvaluateParenExpression(p),
 91433        StringConstantExpressionAst s => s.Value,
 01434        ExpandableStringExpressionAst e => e.Value,
 11435        ConstantExpressionAst c => c.Value,
 01436        VariableExpressionAst v => EvaluateVariableExpression(v),
 01437        MemberExpressionAst me => TryEvaluateMemberExpression(me) ?? me.Extent.Text.Trim(),
 01438        TypeExpressionAst te => ResolveTypeFromName(te.TypeName.FullName) is { } t ? t : te.TypeName.FullName,
 01439        _ => expr.Extent.Text.Trim()
 121440    };
 1441
 1442    /// <summary>
 1443    /// Evaluates a parenthesized expression AST to a runtime value.
 1444    /// </summary>
 1445    /// <param name="p">The parenthesized expression AST to evaluate</param>
 1446    /// <returns>The evaluated runtime value</returns>
 1447    private static object? EvaluateParenExpression(ParenExpressionAst p)
 1448    {
 1449        // Parenthesized values frequently appear in attribute args: ContentType = ('a','b')
 1450        // In the AST, these often surface as a pipeline containing a CommandExpressionAst.
 11451        if (p.Pipeline is PipelineAst pipeline && pipeline.PipelineElements is { Count: 1 } elems && elems[0] is Command
 1452        {
 11453            return EvaluateArgumentExpression(ce.Expression);
 1454        }
 1455        // Fallback: return the raw text inside the parentheses.
 01456        return p.Extent.Text.Trim();
 1457    }
 1458
 1459    /// <summary>
 1460    /// Evaluates a variable expression AST to a runtime value.
 1461    /// </summary>
 1462    /// <param name="v">The variable expression AST to evaluate</param>
 1463    /// <returns>The evaluated runtime value</returns>
 1464    private static object? EvaluateVariableExpression(VariableExpressionAst v)
 1465    {
 01466        var name = v.VariablePath.UserPath;
 01467        if (string.Equals(name, "null", StringComparison.OrdinalIgnoreCase))
 1468        {
 01469            return null;
 1470        }
 01471        if (string.Equals(name, "true", StringComparison.OrdinalIgnoreCase))
 1472        {
 01473            return true;
 1474        }
 01475        if (string.Equals(name, "false", StringComparison.OrdinalIgnoreCase))
 1476        {
 01477            return false;
 1478        }
 1479
 1480        // Unknown variable, preserve source text (e.g. $foo)
 01481        return v.Extent.Text.Trim();
 1482    }
 1483
 1484    private static object? TryEvaluateMemberExpression(MemberExpressionAst me)
 1485    {
 1486        // Common pattern in scripts: [SomeType]::Member
 01487        if (me.Expression is not TypeExpressionAst te)
 1488        {
 01489            return null;
 1490        }
 1491
 01492        var type = ResolveTypeFromName(te.TypeName.FullName);
 01493        if (type is null)
 1494        {
 01495            return null;
 1496        }
 1497
 01498        var memberName = me.Member.Extent.Text.Trim().Trim('"', '\'');
 01499        if (type.IsEnum)
 1500        {
 1501            try
 1502            {
 01503                return Enum.Parse(type, memberName, ignoreCase: true);
 1504            }
 01505            catch
 1506            {
 01507                return null;
 1508            }
 1509        }
 1510
 1511        const BindingFlags Flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase;
 01512        var f = type.GetField(memberName, Flags);
 01513        if (f is not null)
 1514        {
 01515            return f.GetValue(null);
 1516        }
 01517        var p = type.GetProperty(memberName, Flags);
 01518        return p?.GetValue(null);
 01519    }
 1520
 1521    /// <summary>
 1522    /// Converts a raw evaluated value to the specified property type, handling arrays and enums.
 1523    /// </summary>
 1524    /// <param name="raw">The raw evaluated value.</param>
 1525    /// <param name="propertyType">The target property type to convert to.</param>
 1526    /// <returns>The converted value, or null if conversion is not possible.</returns>
 1527    private static object? ConvertToPropertyType(object? raw, Type propertyType)
 1528    {
 101529        if (raw is null)
 1530        {
 01531            return null;
 1532        }
 1533
 101534        var targetType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
 1535
 1536        // Arrays (e.g. string[] ContentType)
 101537        if (targetType.IsArray)
 1538        {
 11539            return ConvertArrayValue(raw, targetType);
 1540        }
 1541
 1542        // If already the right type
 91543        if (targetType.IsInstanceOfType(raw))
 1544        {
 61545            return raw;
 1546        }
 1547
 1548        // Enums
 31549        if (targetType.IsEnum)
 1550        {
 31551            return ConvertEnumValue(raw, targetType);
 1552        }
 1553
 1554        // Boolean strings like "$true" / "$false"
 01555        if (targetType == typeof(bool) && raw is string bs)
 1556        {
 01557            var boolVal = TryParseBooleanString(bs);
 01558            if (boolVal.HasValue)
 1559            {
 01560                return boolVal.Value;
 1561            }
 1562        }
 1563
 1564        // Fallback: ChangeType or keep raw on failure
 01565        return ChangeTypeOrRaw(raw, targetType);
 1566    }
 1567
 1568    /// <summary>
 1569    /// Converts a value to an array of the specified array type, handling string-list parsing,
 1570    /// IEnumerable conversion, and scalar single-element arrays.
 1571    /// </summary>
 1572    /// <param name="raw">The raw value to convert.</param>
 1573    /// <param name="arrayType">The target array type.</param>
 1574    /// <returns>The converted array value.</returns>
 1575    private static object ConvertArrayValue(object raw, Type arrayType)
 1576    {
 11577        var elementType = arrayType.GetElementType() ?? typeof(object);
 1578
 1579        // If already the right array type, keep it
 11580        if (arrayType.IsInstanceOfType(raw))
 1581        {
 01582            return raw;
 1583        }
 1584
 1585        // Parse string list for string[] scenarios
 11586        if (raw is string s && elementType == typeof(string))
 1587        {
 01588            var parsed = TryParseStringList(s);
 01589            if (parsed is not null)
 1590            {
 01591                return parsed;
 1592            }
 1593            // Treat scalar string as single-element array
 01594            return new[] { s };
 1595        }
 1596
 1597        // IEnumerable -> array (avoid string as IEnumerable<char>)
 11598        if (raw is IEnumerable enumerable and not string)
 1599        {
 11600            return ConvertEnumerableToArray(enumerable, elementType);
 1601        }
 1602
 1603        // Scalar -> single-element array
 01604        var single = ConvertToPropertyType(raw, elementType);
 01605        var singleArr = Array.CreateInstance(elementType, 1);
 01606        singleArr.SetValue(single, 0);
 01607        return singleArr;
 1608    }
 1609
 1610    /// <summary>
 1611    /// Attempts to parse a textual list representation such as "('a','b')" or "@('a','b')" into a string array.
 1612    /// Returns null when the input is not a recognized list format.
 1613    /// </summary>
 1614    /// <param name="listText">The input list text.</param>
 1615    /// <returns>A string array if parsing succeeds; otherwise null.</returns>
 1616    private static string[]? TryParseStringList(string listText)
 1617    {
 01618        var t = listText.Trim();
 01619        var hasParens = (t.StartsWith("@(", StringComparison.Ordinal) || t.StartsWith('(')) && t.EndsWith(')');
 01620        if (!hasParens || !t.Contains(',', StringComparison.Ordinal))
 1621        {
 01622            return null;
 1623        }
 1624
 01625        var inner = t.StartsWith("@(", StringComparison.Ordinal) ? t[2..^1] : t[1..^1];
 01626        var parts = inner
 01627            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
 01628            .Select(p => p.Trim().Trim('\'', '"'))
 01629            .Where(p => !string.IsNullOrWhiteSpace(p))
 01630            .ToArray();
 1631
 01632        return parts.Length > 0 ? parts : null;
 1633    }
 1634
 1635    /// <summary>
 1636    /// Converts an IEnumerable to an array of the specified element type, recursively converting each item.
 1637    /// </summary>
 1638    /// <param name="enumerable">The enumerable of items.</param>
 1639    /// <param name="elementType">The target element type.</param>
 1640    /// <returns>An array instance with converted items.</returns>
 1641    private static object ConvertEnumerableToArray(IEnumerable enumerable, Type elementType)
 1642    {
 11643        var items = new List<object?>();
 61644        foreach (var item in enumerable)
 1645        {
 21646            items.Add(item);
 1647        }
 1648
 11649        var arr = Array.CreateInstance(elementType, items.Count);
 61650        for (var i = 0; i < items.Count; i++)
 1651        {
 21652            var convertedItem = ConvertToPropertyType(items[i], elementType);
 21653            arr.SetValue(convertedItem, i);
 1654        }
 11655        return arr;
 1656    }
 1657
 1658    /// <summary>
 1659    /// Converts a raw value to the specified enum type.
 1660    /// </summary>
 1661    /// <param name="raw">The raw value (string or numeric).</param>
 1662    /// <param name="enumType">The target enum type.</param>
 1663    /// <returns>An enum instance parsed from the raw value.</returns>
 1664    private static object ConvertEnumValue(object raw, Type enumType)
 1665    {
 31666        return raw is string s
 31667            ? Enum.Parse(enumType, s, ignoreCase: true)
 31668            : Enum.ToObject(enumType, raw);
 1669    }
 1670
 1671    /// <summary>
 1672    /// Attempts to parse a boolean value from PowerShell-like string tokens ("$true", "$false", "true", "false").
 1673    /// Returns null if the string is not a recognized boolean token.
 1674    /// </summary>
 1675    /// <param name="text">The input text.</param>
 1676    /// <returns>True/False for recognized tokens; otherwise null.</returns>
 1677    private static bool? TryParseBooleanString(string text)
 1678    {
 01679        if (string.Equals(text, "$true", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "true", StringCompar
 1680        {
 01681            return true;
 1682        }
 01683        if (string.Equals(text, "$false", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "false", StringComp
 1684        {
 01685            return false;
 1686        }
 1687        // Unrecognized token
 01688        return null;
 1689    }
 1690
 1691    /// <summary>
 1692    /// Tries to convert a value to the target type using Convert.ChangeType; if conversion fails, returns the original 
 1693    /// </summary>
 1694    /// <param name="raw">The raw value.</param>
 1695    /// <param name="targetType">The target type.</param>
 1696    /// <returns>The converted value or the original raw value on failure.</returns>
 1697    private static object ChangeTypeOrRaw(object raw, Type targetType)
 1698    {
 1699        try
 1700        {
 01701            return Convert.ChangeType(raw, targetType);
 1702        }
 01703        catch
 1704        {
 1705            // Best-effort: keep raw rather than failing the scan.
 01706            return raw;
 1707        }
 01708    }
 1709
 1710    /// <summary>
 1711    /// Resolves a KestrunAnnotation-derived type from an AttributeAst.
 1712    /// </summary>
 1713    /// <param name="attr">The AttributeAst to resolve the type from.</param>
 1714    /// <returns>The resolved type if found; otherwise, null.</returns>
 1715    private static Type? ResolveKestrunAnnotationType(AttributeAst attr)
 1716    {
 1717        // PowerShell attribute syntax allows omitting the 'Attribute' suffix.
 41718        var shortName = attr.TypeName.Name;
 1719
 1720        // If a namespace-qualified name is present, try it directly.
 41721        var fullName = attr.TypeName.FullName;
 1722
 41723        var type = ResolveTypeFromName(fullName);
 41724        type ??= ResolveTypeFromName(shortName);
 41725        if (type is null && !shortName.EndsWith("Attribute", StringComparison.OrdinalIgnoreCase))
 1726        {
 41727            type ??= ResolveTypeFromName(shortName + "Attribute");
 1728        }
 1729
 41730        return type is not null && typeof(KestrunAnnotation).IsAssignableFrom(type) ? type : null;
 1731    }
 1732
 1733    /// <summary>
 1734    /// Resolves a CmdletMetadataAttribute-derived type from an AttributeAst.
 1735    /// </summary>
 1736    /// <param name="attr">The AttributeAst to resolve the type from.</param>
 1737    /// <returns>The resolved type if found; otherwise, null.</returns>
 1738    private static Type? ResolveCmdletMetadataAttributeType(AttributeAst attr)
 1739    {
 1740        // PowerShell attribute syntax allows omitting the 'Attribute' suffix.
 01741        var shortName = attr.TypeName.Name;
 1742
 1743        // If a namespace-qualified name is present, try it directly.
 01744        var fullName = attr.TypeName.FullName;
 1745
 01746        var type = ResolveTypeFromName(fullName);
 01747        type ??= ResolveTypeFromName(shortName);
 01748        if (type is null && !shortName.EndsWith("Attribute", StringComparison.OrdinalIgnoreCase))
 1749        {
 01750            type ??= ResolveTypeFromName(shortName + "Attribute");
 1751        }
 1752
 01753        return typeof(CmdletMetadataAttribute).IsAssignableFrom(type)
 01754             ? type : null;
 1755    }
 1756
 1757    private static Type? ResolveTypeFromName(string name)
 1758    {
 121759        if (string.IsNullOrWhiteSpace(name))
 1760        {
 01761            return null;
 1762        }
 1763
 1764        // Try Type.GetType first (works for assembly-qualified names)
 121765        var t = Type.GetType(name, throwOnError: false, ignoreCase: true);
 121766        if (t is not null)
 1767        {
 01768            return t;
 1769        }
 1770
 1771        // Search loaded assemblies (best-effort)
 49961772        foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
 1773        {
 24881774            t = asm.GetType(name, throwOnError: false, ignoreCase: true);
 24881775            if (t is not null)
 1776            {
 41777                return t;
 1778            }
 1779
 1780            // Also allow matching by short type name.
 1781            try
 1782            {
 4167841783                t = asm.GetTypes().FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
 24761784                if (t is not null)
 1785                {
 01786                    return t;
 1787                }
 24761788            }
 81789            catch
 1790            {
 1791                // Some dynamic/reflection-only assemblies can throw on GetTypes(). Ignore.
 81792            }
 1793        }
 1794
 81795        return null;
 01796    }
 1797
 1798    /// <summary>
 1799    /// Determines if an attribute matches the provided filter.
 1800    /// </summary>
 1801    /// <param name="attr">The attribute to check.</param>
 1802    /// <param name="filter">The filter of attribute type names to match against.</param>
 1803    /// <returns>True if the attribute matches the filter; otherwise, false.</returns>
 1804    private static bool IsMatchingAttribute(AttributeAst attr, IReadOnlyCollection<string>? filter)
 1805    {
 41806        if (filter is null || filter.Count == 0)
 1807        {
 41808            return true;
 1809        }
 1810
 1811        // Compare short type name: [ComponentParameter] or [Namespace.ComponentParameter]
 01812        var shortName = attr.TypeName.Name;
 01813        return filter.Any(x => string.Equals(x, shortName, StringComparison.OrdinalIgnoreCase));
 1814    }
 1815
 1816    /// <summary>
 1817    /// Tries to parse a standalone attribute line from a statement text.
 1818    /// </summary>
 1819    /// <param name="statementText">The text of the statement to parse for standalone attributes.</param>
 1820    /// <returns>An array of AttributeAst instances if parsing is successful; otherwise, an empty array.</returns>
 1821    private static IReadOnlyList<AttributeAst> TryParseStandaloneAttributeLine(string statementText)
 1822    {
 11823        var t = statementText.Trim();
 1824
 1825        // Basic "looks like our DSL annotation"
 11826        if (!t.StartsWith('[') ||
 11827            !t.EndsWith(']') ||
 11828            !t.Contains('(', StringComparison.Ordinal))
 1829        {
 11830            return [];
 1831        }
 1832
 1833        // Parse in a context where attributes are legal (param block)
 01834        var synthetic = "param(\n" + t + "\n[object]$__x\n)\n";
 1835
 01836        var ast = Parser.ParseInput(synthetic, out _, out var errors);
 01837        if (errors is { Length: > 0 })
 1838        {
 01839            return [];
 1840        }
 1841
 01842        var paramBlock = ast.Find(n => n is ParamBlockAst, searchNestedScriptBlocks: true) as ParamBlockAst;
 01843        var firstParam = paramBlock?.Parameters?.FirstOrDefault();
 01844        return firstParam?.Attributes?
 01845            .OfType<AttributeAst>()
 01846            .ToArray()
 01847        ?? [];
 1848    }
 1849}

Methods/Properties

.ctor(System.String)
get_Annotations()
get_Name()
get_VariableType()
get_VariableTypeName()
get_InitialValue()
get_InitialValueExpression()
get_NoDefault()
ScanFromRunningScriptOrPath(System.Management.Automation.EngineIntrinsics,System.String,System.Collections.Generic.IReadOnlyCollection`1<System.String>,System.String,System.Int32)
ScanFromPath(System.String,System.Collections.Generic.IReadOnlyCollection`1<System.String>,System.String,System.Int32)
ParseFile(System.String,System.Management.Automation.Language.ParseError[]&)
FindDotSourcedFiles()
ResolveDotSourcedFilesFromCommand()
TryResolveDotSourcedPathFromElements(System.Collections.Generic.IReadOnlyList`1<System.Management.Automation.Language.CommandElementAst>,System.Int32,System.String,System.String&)
TryGetSimpleString(System.Management.Automation.Language.CommandElementAst)
ResolveDotSourcedPath(System.String,System.String)
ExtractAnnotationsFromAst(System.Management.Automation.Language.ScriptBlockAst,System.Collections.Generic.Dictionary`2<System.String,Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable>,System.Collections.Generic.IReadOnlyCollection`1<System.String>,System.String,System.Boolean)
GetNamedBlocks(System.Management.Automation.Language.ScriptBlockAst)
GetTopLevelStatements(System.Management.Automation.Language.Ast)
TryHandleInlineAttributedAssignment(System.Management.Automation.Language.StatementAst,System.Collections.Generic.Dictionary`2<System.String,Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable>,System.Collections.Generic.IReadOnlyCollection`1<System.String>,System.String,System.Collections.Generic.List`1<System.Management.Automation.Language.AttributeAst>)
TryHandleInlineAttributedDeclaration(System.Management.Automation.Language.StatementAst,System.Collections.Generic.Dictionary`2<System.String,Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable>,System.Collections.Generic.IReadOnlyCollection`1<System.String>,System.String,System.Collections.Generic.List`1<System.Management.Automation.Language.AttributeAst>)
TryHandleStandaloneAttributeLine(System.Management.Automation.Language.StatementAst,System.Collections.Generic.IReadOnlyCollection`1<System.String>,System.Collections.Generic.List`1<System.Management.Automation.Language.AttributeAst>)
TryHandleVariableAssignment(System.Management.Automation.Language.StatementAst,System.Collections.Generic.Dictionary`2<System.String,Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable>,System.String,System.Collections.Generic.List`1<System.Management.Automation.Language.AttributeAst>)
ApplyVariableTypeInfo(Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable,System.Type,System.String)
ApplyInitializerValues(Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable,System.Object,System.String)
ApplyPendingAnnotations(Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable,System.Collections.Generic.List`1<System.Management.Automation.Language.AttributeAst>,System.String,System.String)
TryHandleDeclarationOnlyVariable(System.Management.Automation.Language.StatementAst,System.Collections.Generic.Dictionary`2<System.String,Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable>,System.String,System.Collections.Generic.List`1<System.Management.Automation.Language.AttributeAst>)
GetOrCreateVariable(System.Collections.Generic.Dictionary`2<System.String,Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable>,System.String)
TryExtractInlineAttributedDeclaration(System.Management.Automation.Language.StatementAst,System.Collections.Generic.IReadOnlyCollection`1<System.String>,System.String&,System.Type&,System.String&,System.Collections.Generic.IReadOnlyList`1<System.Management.Automation.Language.AttributeAst>&)
TryExtractExpressionFromStatement(System.Management.Automation.Language.StatementAst,System.Management.Automation.Language.ExpressionAst&)
CollectMatchingAttributesFromChain(System.Management.Automation.Language.ExpressionAst,System.Collections.Generic.IReadOnlyCollection`1<System.String>)
TryExtractInlineAttributedAssignment(System.Management.Automation.Language.AssignmentStatementAst,System.Collections.Generic.IReadOnlyCollection`1<System.String>,System.String&,System.Type&,System.String&,System.Collections.Generic.IReadOnlyList`1<System.Management.Automation.Language.AttributeAst>&)
TryGetAssignmentTarget(System.Management.Automation.Language.ExpressionAst,System.String&,System.Type&,System.String&)
TryGetDeclaredVariableInfo(System.Management.Automation.Language.ExpressionAst,System.String&,System.Type&,System.String&)
UnwrapAttributedExpressionChain(System.Management.Automation.Language.ExpressionAst&)
TryUnwrapConvertExpression(System.Management.Automation.Language.ExpressionAst&,System.Type&,System.String&)
UnwrapRemainingAttributedExpressions(System.Management.Automation.Language.ExpressionAst&)
TryInferVariableTypeFromAttributes(System.Collections.Generic.IReadOnlyList`1<System.String>,System.Type&,System.String&)
ResolvePowerShellTypeName(System.String)
NormalizePowerShellTypeName(System.String)
EvaluateValueStatement(System.Management.Automation.Language.StatementAst)
TryCreateAnnotation(System.Management.Automation.Language.AttributeAst,System.String,System.String)
TryCreateKestrunAnnotation(System.Management.Automation.Language.AttributeAst,System.String,System.String)
TryGetConstantLikeValue(System.Management.Automation.Language.ExpressionAst)
TryGetPlainConstantValue(System.Management.Automation.Language.ExpressionAst,System.Object&)
TryGetPlainStringValue(System.Management.Automation.Language.ExpressionAst,System.Object&)
TryGetStaticTypeMemberValue(System.Management.Automation.Language.ExpressionAst,System.Object&)
TryGetReflectedPropertyValue(System.Type,System.String,System.Object&)
TryGetReflectedFieldValue(System.Type,System.String,System.Object&)
GetMemberName(System.Management.Automation.Language.CommandElementAst)
TryCmdletMetadataAttribute(System.Management.Automation.Language.AttributeAst)
ApplyDefaultComponentName(System.Attribute,System.String,System.String)
ApplyNamedArgument(KestrunAnnotation,System.Management.Automation.Language.NamedAttributeArgumentAst)
EvaluateArgumentExpression(System.Management.Automation.Language.ExpressionAst)
EvaluateParenExpression(System.Management.Automation.Language.ParenExpressionAst)
EvaluateVariableExpression(System.Management.Automation.Language.VariableExpressionAst)
TryEvaluateMemberExpression(System.Management.Automation.Language.MemberExpressionAst)
ConvertToPropertyType(System.Object,System.Type)
ConvertArrayValue(System.Object,System.Type)
TryParseStringList(System.String)
ConvertEnumerableToArray(System.Collections.IEnumerable,System.Type)
ConvertEnumValue(System.Object,System.Type)
TryParseBooleanString(System.String)
ChangeTypeOrRaw(System.Object,System.Type)
ResolveKestrunAnnotationType(System.Management.Automation.Language.AttributeAst)
ResolveCmdletMetadataAttributeType(System.Management.Automation.Language.AttributeAst)
ResolveTypeFromName(System.String)
IsMatchingAttribute(System.Management.Automation.Language.AttributeAst,System.Collections.Generic.IReadOnlyCollection`1<System.String>)
TryParseStandaloneAttributeLine(System.String)