| | | 1 | | using System.Management.Automation; |
| | | 2 | | using System.Management.Automation.Language; |
| | | 3 | | using System.Reflection; |
| | | 4 | | using System.Collections; |
| | | 5 | | using System.Management.Automation.Internal; |
| | | 6 | | |
| | | 7 | | namespace Kestrun.OpenApi; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// Scans PowerShell script files for OpenAPI component annotations defined via attributes. |
| | | 11 | | /// </summary> |
| | | 12 | | public static class OpenApiComponentAnnotationScanner |
| | | 13 | | { |
| | | 14 | | /// <summary> |
| | | 15 | | /// Represents a variable discovered in script, along with its OpenAPI annotations and metadata. |
| | | 16 | | /// </summary> |
| | 7 | 17 | | public sealed class AnnotatedVariable(string name) |
| | | 18 | | { |
| | | 19 | | /// <summary>Annotations attached to the variable.</summary> |
| | 27 | 20 | | public List<KestrunAnnotation> Annotations { get; } = []; |
| | | 21 | | |
| | | 22 | | /// <summary> |
| | | 23 | | /// The variable name. |
| | | 24 | | /// </summary> |
| | 13 | 25 | | 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> |
| | 19 | 28 | | public Type? VariableType { get; set; } |
| | | 29 | | |
| | | 30 | | /// <summary>The declared variable type name as written in script (best-effort).</summary> |
| | 8 | 31 | | public string? VariableTypeName { get; set; } |
| | | 32 | | |
| | | 33 | | /// <summary>The initializer value if it can be evaluated (best-effort).</summary> |
| | 8 | 34 | | public object? InitialValue { get; set; } |
| | | 35 | | |
| | | 36 | | /// <summary>The initializer expression text (always available when an initializer exists).</summary> |
| | 2 | 37 | | public string? InitialValueExpression { get; set; } |
| | | 38 | | /// <summary>Indicates whether the variable was declared with no default (e.g. <c>$x = [NoDefault]</c>).</summar |
| | 11 | 39 | | 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 |
| | 0 | 68 | | var psCommandPath = engine.SessionState.PSVariable.GetValue("PSCommandPath") as string; |
| | 0 | 69 | | var entry = mainPath ?? psCommandPath; |
| | | 70 | | |
| | 0 | 71 | | if (string.IsNullOrWhiteSpace(entry)) |
| | | 72 | | { |
| | 0 | 73 | | throw new InvalidOperationException("No running script path found ($PSCommandPath is empty) and no mainPath |
| | | 74 | | } |
| | | 75 | | |
| | 0 | 76 | | entry = Path.GetFullPath(entry); |
| | | 77 | | |
| | 0 | 78 | | 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 | | { |
| | 3 | 103 | | mainPath = Path.GetFullPath(mainPath); |
| | | 104 | | |
| | 3 | 105 | | var variables = new Dictionary<string, AnnotatedVariable>(StringComparer.OrdinalIgnoreCase); |
| | | 106 | | |
| | 3 | 107 | | var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase); |
| | 3 | 108 | | var queue = new Queue<string>(); |
| | | 109 | | |
| | 3 | 110 | | _ = visited.Add(mainPath); |
| | 3 | 111 | | queue.Enqueue(mainPath); |
| | | 112 | | |
| | 7 | 113 | | while (queue.Count > 0) |
| | | 114 | | { |
| | 4 | 115 | | if (visited.Count > maxFiles) |
| | | 116 | | { |
| | 0 | 117 | | throw new InvalidOperationException($"Exceeded maxFiles={maxFiles} while following dot-sourced scripts. |
| | | 118 | | } |
| | | 119 | | |
| | 4 | 120 | | var file = queue.Dequeue(); |
| | 4 | 121 | | 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 | | |
| | 4 | 126 | | ExtractAnnotationsFromAst(ast, variables, attributeTypeFilter, componentNameArgument); |
| | | 127 | | |
| | 10 | 128 | | foreach (var inc in FindDotSourcedFiles(ast, file)) |
| | | 129 | | { |
| | 1 | 130 | | if (inc is null) |
| | | 131 | | { |
| | | 132 | | continue; |
| | | 133 | | } |
| | | 134 | | |
| | 1 | 135 | | if (visited.Add(inc)) |
| | | 136 | | { |
| | 1 | 137 | | queue.Enqueue(inc); |
| | | 138 | | } |
| | | 139 | | } |
| | | 140 | | } |
| | | 141 | | |
| | 3 | 142 | | 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 | | { |
| | 4 | 155 | | var text = File.ReadAllText(path); |
| | 4 | 156 | | 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. |
| | 4 | 158 | | 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 | | { |
| | 4 | 172 | | var baseDir = Path.GetDirectoryName(currentFilePath) ?? Directory.GetCurrentDirectory(); |
| | | 173 | | |
| | | 174 | | var commands = ast.FindAll(n => n is CommandAst, searchNestedScriptBlocks: true) |
| | 4 | 175 | | .Cast<CommandAst>(); |
| | | 176 | | |
| | 14 | 177 | | foreach (var cmd in commands) |
| | | 178 | | { |
| | 8 | 179 | | foreach (var resolved in ResolveDotSourcedFilesFromCommand(cmd, baseDir)) |
| | | 180 | | { |
| | 1 | 181 | | yield return resolved; |
| | | 182 | | } |
| | | 183 | | } |
| | 4 | 184 | | } |
| | | 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. |
| | 3 | 197 | | if (cmd.InvocationOperator == TokenKind.Dot) |
| | | 198 | | { |
| | 1 | 199 | | if (TryResolveDotSourcedPathFromElements(cmd.CommandElements, elementIndex: 0, baseDir, out var resolved)) |
| | | 200 | | { |
| | 1 | 201 | | yield return resolved; |
| | | 202 | | } |
| | | 203 | | |
| | 1 | 204 | | yield break; |
| | | 205 | | } |
| | | 206 | | |
| | | 207 | | // Back-compat / best-effort: some AST shapes could represent '.' as a command element. |
| | 2 | 208 | | var elems = cmd.CommandElements; |
| | 2 | 209 | | if (elems.Count < 2) |
| | | 210 | | { |
| | 2 | 211 | | yield break; |
| | | 212 | | } |
| | | 213 | | |
| | 0 | 214 | | if (!string.Equals(elems[0].Extent.Text.Trim(), ".", StringComparison.Ordinal)) |
| | | 215 | | { |
| | 0 | 216 | | yield break; |
| | | 217 | | } |
| | | 218 | | |
| | 0 | 219 | | if (TryResolveDotSourcedPathFromElements(elems, elementIndex: 1, baseDir, out var resolvedCompat)) |
| | | 220 | | { |
| | 0 | 221 | | yield return resolvedCompat; |
| | | 222 | | } |
| | 0 | 223 | | } |
| | | 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 | | { |
| | 1 | 235 | | resolved = null; |
| | | 236 | | |
| | 1 | 237 | | if (elements.Count <= elementIndex) |
| | | 238 | | { |
| | 0 | 239 | | return false; |
| | | 240 | | } |
| | | 241 | | |
| | 1 | 242 | | var raw = TryGetSimpleString(elements[elementIndex]); |
| | 1 | 243 | | if (raw is null) |
| | | 244 | | { |
| | 0 | 245 | | return false; |
| | | 246 | | } |
| | | 247 | | |
| | 1 | 248 | | resolved = ResolveDotSourcedPath(raw, baseDir); |
| | 1 | 249 | | 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\"" |
| | 1 | 260 | | return element switch |
| | 1 | 261 | | { |
| | 0 | 262 | | StringConstantExpressionAst s => s.Value, |
| | 1 | 263 | | ExpandableStringExpressionAst e => e.Value,// contains "$PSScriptRoot\foo.ps1" as text; we do best-effort re |
| | 0 | 264 | | _ => null, |
| | 1 | 265 | | }; |
| | | 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 | | { |
| | 1 | 276 | | var t = raw.Trim(); |
| | | 277 | | |
| | | 278 | | // Expand the common tokens best-effort (no general expression evaluation!) |
| | 1 | 279 | | t = t.Replace("$PSScriptRoot", baseDir, StringComparison.OrdinalIgnoreCase); |
| | 1 | 280 | | 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 |
| | 1 | 284 | | t = t.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); |
| | | 285 | | |
| | | 286 | | // Relative -> baseDir |
| | 1 | 287 | | if (!Path.IsPathRooted(t)) |
| | | 288 | | { |
| | 0 | 289 | | t = Path.Combine(baseDir, t); |
| | | 290 | | } |
| | | 291 | | |
| | | 292 | | try |
| | | 293 | | { |
| | 1 | 294 | | var full = Path.GetFullPath(t); |
| | 1 | 295 | | return File.Exists(full) ? full : null; |
| | | 296 | | } |
| | 0 | 297 | | catch |
| | | 298 | | { |
| | 0 | 299 | | return null; |
| | | 300 | | } |
| | 1 | 301 | | } |
| | | 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. |
| | 16 | 320 | | foreach (var block in GetNamedBlocks(scriptAst)) |
| | | 321 | | { |
| | 4 | 322 | | var statements = GetTopLevelStatements(block); |
| | | 323 | | |
| | 4 | 324 | | var pending = new List<AttributeAst>(); |
| | | 325 | | |
| | 18 | 326 | | foreach (var st in statements) |
| | | 327 | | { |
| | 5 | 328 | | if (TryHandleInlineAttributedAssignment(st, variables, attributeTypeFilter, componentNameArgument, pendi |
| | | 329 | | { |
| | | 330 | | continue; |
| | | 331 | | } |
| | | 332 | | |
| | 2 | 333 | | if (TryHandleInlineAttributedDeclaration(st, variables, attributeTypeFilter, componentNameArgument, pend |
| | | 334 | | { |
| | | 335 | | continue; |
| | | 336 | | } |
| | | 337 | | |
| | 1 | 338 | | if (TryHandleStandaloneAttributeLine(st, attributeTypeFilter, pending)) |
| | | 339 | | { |
| | | 340 | | continue; |
| | | 341 | | } |
| | | 342 | | |
| | 1 | 343 | | if (TryHandleVariableAssignment(st, variables, componentNameArgument, pending)) |
| | | 344 | | { |
| | | 345 | | continue; |
| | | 346 | | } |
| | | 347 | | |
| | 1 | 348 | | if (TryHandleDeclarationOnlyVariable(st, variables, componentNameArgument, pending)) |
| | | 349 | | { |
| | | 350 | | continue; |
| | | 351 | | } |
| | | 352 | | |
| | | 353 | | // strict: anything else clears pending |
| | 1 | 354 | | if (strict && pending.Count > 0) |
| | | 355 | | { |
| | 0 | 356 | | pending.Clear(); |
| | | 357 | | } |
| | | 358 | | } |
| | | 359 | | } |
| | 4 | 360 | | } |
| | | 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 | | { |
| | 4 | 369 | | var blocks = new List<Ast>(); |
| | | 370 | | |
| | 4 | 371 | | if (scriptAst.BeginBlock is not null) |
| | | 372 | | { |
| | 0 | 373 | | blocks.Add(scriptAst.BeginBlock); |
| | | 374 | | } |
| | | 375 | | |
| | 4 | 376 | | if (scriptAst.ProcessBlock is not null) |
| | | 377 | | { |
| | 0 | 378 | | blocks.Add(scriptAst.ProcessBlock); |
| | | 379 | | } |
| | | 380 | | |
| | 4 | 381 | | if (scriptAst.EndBlock is not null) |
| | | 382 | | { |
| | 4 | 383 | | blocks.Add(scriptAst.EndBlock); |
| | | 384 | | } |
| | | 385 | | |
| | 4 | 386 | | 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) |
| | 66 | 395 | | => [.. block.FindAll(n => n is StatementAst sa && sa.Parent == block, searchNestedScriptBlocks: false) |
| | 4 | 396 | | .Cast<StatementAst>() |
| | 9 | 397 | | .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 | | { |
| | 5 | 415 | | if (statement is not AssignmentStatementAst inlineAssign) |
| | | 416 | | { |
| | 2 | 417 | | return false; |
| | | 418 | | } |
| | | 419 | | |
| | 3 | 420 | | if (!TryExtractInlineAttributedAssignment(inlineAssign, attributeTypeFilter, out var inlineVarName, out var inli |
| | | 421 | | { |
| | 0 | 422 | | return false; |
| | | 423 | | } |
| | | 424 | | |
| | 3 | 425 | | var (initValue, initExpr) = EvaluateValueStatement(inlineAssign.Right); |
| | 3 | 426 | | var entry = GetOrCreateVariable(variables, inlineVarName); |
| | 3 | 427 | | entry.VariableType ??= inlineVarType; |
| | 3 | 428 | | entry.VariableTypeName ??= inlineVarTypeName; |
| | | 429 | | |
| | 3 | 430 | | if (initExpr != "NoDefault") |
| | | 431 | | { |
| | 1 | 432 | | entry.NoDefault = false; |
| | 1 | 433 | | entry.InitialValue ??= initValue; |
| | 1 | 434 | | entry.InitialValueExpression ??= initExpr; |
| | | 435 | | } |
| | | 436 | | else |
| | | 437 | | { |
| | 2 | 438 | | entry.NoDefault = true; |
| | | 439 | | } |
| | | 440 | | |
| | 12 | 441 | | foreach (var a in inlineAttrs) |
| | | 442 | | { |
| | 3 | 443 | | var ka = TryCreateAnnotation(a, defaultComponentName: inlineVarName, componentNameArgument); |
| | 3 | 444 | | if (ka is not null) |
| | | 445 | | { |
| | 3 | 446 | | entry.Annotations.Add(ka); |
| | | 447 | | } |
| | | 448 | | } |
| | | 449 | | |
| | 3 | 450 | | pending.Clear(); |
| | 3 | 451 | | 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 | | { |
| | 2 | 470 | | if (!TryExtractInlineAttributedDeclaration(statement, attributeTypeFilter, out var varName, out var varType, out |
| | | 471 | | { |
| | 1 | 472 | | return false; |
| | | 473 | | } |
| | | 474 | | |
| | 1 | 475 | | var entry = GetOrCreateVariable(variables, varName); |
| | 1 | 476 | | entry.VariableType ??= varType; |
| | 1 | 477 | | entry.VariableTypeName ??= varTypeName; |
| | | 478 | | |
| | 4 | 479 | | foreach (var a in attrs) |
| | | 480 | | { |
| | 1 | 481 | | var ka = TryCreateAnnotation(a, defaultComponentName: varName, componentNameArgument); |
| | 1 | 482 | | if (ka is not null) |
| | | 483 | | { |
| | 1 | 484 | | entry.Annotations.Add(ka); |
| | | 485 | | } |
| | | 486 | | } |
| | | 487 | | |
| | 1 | 488 | | pending.Clear(); |
| | 1 | 489 | | 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 | | { |
| | 1 | 504 | | var parsedAttrs = TryParseStandaloneAttributeLine(statement.Extent.Text); |
| | 1 | 505 | | if (parsedAttrs.Count == 0) |
| | | 506 | | { |
| | 1 | 507 | | return false; |
| | | 508 | | } |
| | | 509 | | |
| | 0 | 510 | | foreach (var a in parsedAttrs) |
| | | 511 | | { |
| | 0 | 512 | | if (IsMatchingAttribute(a, attributeTypeFilter)) |
| | | 513 | | { |
| | 0 | 514 | | pending.Add(a); |
| | | 515 | | } |
| | | 516 | | } |
| | | 517 | | |
| | 0 | 518 | | 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 | | { |
| | 1 | 535 | | if (statement is not AssignmentStatementAst assign) |
| | | 536 | | { |
| | 1 | 537 | | return false; |
| | | 538 | | } |
| | | 539 | | |
| | 0 | 540 | | if (!TryGetAssignmentTarget(assign.Left, out var targetVarName, out var targetVarType, out var targetVarTypeName |
| | | 541 | | { |
| | 0 | 542 | | return false; |
| | | 543 | | } |
| | | 544 | | |
| | 0 | 545 | | var shouldCapture = pending.Count > 0 || variables.ContainsKey(targetVarName); |
| | 0 | 546 | | if (!shouldCapture) |
| | | 547 | | { |
| | 0 | 548 | | return false; |
| | | 549 | | } |
| | | 550 | | |
| | 0 | 551 | | var (initValue, initExpr) = EvaluateValueStatement(assign.Right); |
| | 0 | 552 | | var entry = GetOrCreateVariable(variables, targetVarName); |
| | | 553 | | |
| | 0 | 554 | | ApplyVariableTypeInfo(entry, targetVarType, targetVarTypeName); |
| | 0 | 555 | | ApplyInitializerValues(entry, initValue, initExpr); |
| | 0 | 556 | | ApplyPendingAnnotations(entry, pending, targetVarName, componentNameArgument); |
| | | 557 | | |
| | 0 | 558 | | pending.Clear(); |
| | 0 | 559 | | 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 | | { |
| | 0 | 570 | | entry.VariableType ??= variableType; |
| | 0 | 571 | | entry.VariableTypeName ??= variableTypeName; |
| | 0 | 572 | | } |
| | | 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 | | { |
| | 0 | 582 | | if (initExpr == "NoDefault") |
| | | 583 | | { |
| | 0 | 584 | | entry.NoDefault = true; |
| | 0 | 585 | | return; |
| | | 586 | | } |
| | | 587 | | |
| | 0 | 588 | | entry.NoDefault = false; |
| | 0 | 589 | | entry.InitialValue ??= initValue; |
| | 0 | 590 | | entry.InitialValueExpression ??= initExpr; |
| | 0 | 591 | | } |
| | | 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 | | { |
| | 0 | 606 | | if (pending.Count == 0) |
| | | 607 | | { |
| | 0 | 608 | | return; |
| | | 609 | | } |
| | | 610 | | |
| | 0 | 611 | | foreach (var a in pending) |
| | | 612 | | { |
| | 0 | 613 | | var ka = TryCreateAnnotation(a, defaultComponentName: targetVarName, componentNameArgument); |
| | 0 | 614 | | if (ka is not null) |
| | | 615 | | { |
| | 0 | 616 | | entry.Annotations.Add(ka); |
| | | 617 | | } |
| | | 618 | | } |
| | 0 | 619 | | } |
| | | 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 | | { |
| | 1 | 635 | | if (pending.Count == 0) |
| | | 636 | | { |
| | 1 | 637 | | return false; |
| | | 638 | | } |
| | | 639 | | |
| | 0 | 640 | | if (statement is not CommandExpressionAst declExpr) |
| | | 641 | | { |
| | 0 | 642 | | return false; |
| | | 643 | | } |
| | | 644 | | |
| | 0 | 645 | | if (!TryGetDeclaredVariableInfo(declExpr.Expression, out var declaredVarName, out var declaredVarType, out var d |
| | | 646 | | { |
| | 0 | 647 | | return false; |
| | | 648 | | } |
| | | 649 | | |
| | 0 | 650 | | var entry = GetOrCreateVariable(variables, declaredVarName); |
| | 0 | 651 | | entry.VariableType ??= declaredVarType; |
| | 0 | 652 | | entry.VariableTypeName ??= declaredVarTypeName; |
| | | 653 | | |
| | 0 | 654 | | foreach (var a in pending) |
| | | 655 | | { |
| | 0 | 656 | | var ka = TryCreateAnnotation(a, defaultComponentName: declaredVarName, componentNameArgument); |
| | 0 | 657 | | if (ka is not null) |
| | | 658 | | { |
| | 0 | 659 | | entry.Annotations.Add(ka); |
| | | 660 | | } |
| | | 661 | | } |
| | | 662 | | |
| | 0 | 663 | | pending.Clear(); |
| | 0 | 664 | | 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 | | { |
| | 4 | 675 | | if (!variables.TryGetValue(varName, out var entry)) |
| | | 676 | | { |
| | 4 | 677 | | entry = new AnnotatedVariable(varName); |
| | 4 | 678 | | variables[varName] = entry; |
| | | 679 | | } |
| | | 680 | | |
| | 4 | 681 | | 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 | | { |
| | 2 | 702 | | variableName = string.Empty; |
| | 2 | 703 | | variableType = null; |
| | 2 | 704 | | variableTypeName = null; |
| | 2 | 705 | | attributes = []; |
| | | 706 | | |
| | 2 | 707 | | if (!TryExtractExpressionFromStatement(statement, out var expr)) |
| | | 708 | | { |
| | 1 | 709 | | return false; |
| | | 710 | | } |
| | | 711 | | |
| | | 712 | | // Check for attributed-expression chain |
| | 1 | 713 | | if (expr is null) |
| | | 714 | | { |
| | 0 | 715 | | return false; |
| | | 716 | | } |
| | | 717 | | |
| | 1 | 718 | | var found = CollectMatchingAttributesFromChain(expr, attributeTypeFilter); |
| | 1 | 719 | | if (found.Count == 0) |
| | | 720 | | { |
| | 0 | 721 | | return false; |
| | | 722 | | } |
| | | 723 | | |
| | 1 | 724 | | if (!TryGetDeclaredVariableInfo(expr, out variableName, out variableType, out variableTypeName)) |
| | | 725 | | { |
| | 0 | 726 | | return false; |
| | | 727 | | } |
| | | 728 | | |
| | 1 | 729 | | attributes = found; |
| | 1 | 730 | | 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 | | { |
| | 2 | 741 | | expr = statement switch |
| | 2 | 742 | | { |
| | 0 | 743 | | CommandExpressionAst ce => ce.Expression, |
| | 3 | 744 | | PipelineAst p when p.PipelineElements is { Count: 1 } && p.PipelineElements[0] is CommandExpressionAst ce => |
| | 1 | 745 | | _ => null |
| | 2 | 746 | | }; |
| | | 747 | | |
| | 2 | 748 | | 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 | | { |
| | 1 | 759 | | var found = new List<AttributeAst>(); |
| | 1 | 760 | | var cursor = expr; |
| | | 761 | | |
| | 3 | 762 | | while (cursor is AttributedExpressionAst aex) |
| | | 763 | | { |
| | 2 | 764 | | if (aex.Attribute is AttributeAst attr && IsMatchingAttribute(attr, attributeTypeFilter)) |
| | | 765 | | { |
| | 1 | 766 | | found.Add(attr); |
| | | 767 | | } |
| | 2 | 768 | | cursor = aex.Child; |
| | 2 | 769 | | } |
| | | 770 | | |
| | 1 | 771 | | 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 | | { |
| | 3 | 792 | | variableName = string.Empty; |
| | 3 | 793 | | variableType = null; |
| | 3 | 794 | | variableTypeName = null; |
| | 3 | 795 | | attributes = []; |
| | | 796 | | |
| | | 797 | | // Collect matching attributes from the left-hand attributed-expression chain. |
| | 3 | 798 | | var found = new List<AttributeAst>(); |
| | 3 | 799 | | var cursor = assignment.Left; |
| | 9 | 800 | | while (cursor is AttributedExpressionAst aex) |
| | | 801 | | { |
| | 6 | 802 | | if (aex.Attribute is AttributeAst attr && IsMatchingAttribute(attr, attributeTypeFilter)) |
| | | 803 | | { |
| | 3 | 804 | | found.Add(attr); |
| | | 805 | | } |
| | 6 | 806 | | cursor = aex.Child; |
| | 6 | 807 | | } |
| | | 808 | | |
| | 3 | 809 | | if (found.Count == 0) |
| | | 810 | | { |
| | 0 | 811 | | return false; |
| | | 812 | | } |
| | | 813 | | |
| | 3 | 814 | | if (!TryGetDeclaredVariableInfo(assignment.Left, out variableName, out variableType, out variableTypeName)) |
| | | 815 | | { |
| | 0 | 816 | | return false; |
| | | 817 | | } |
| | | 818 | | |
| | 3 | 819 | | attributes = found; |
| | 3 | 820 | | 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 |
| | 0 | 835 | | => 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 | | { |
| | 4 | 847 | | variableName = string.Empty; |
| | 4 | 848 | | var cursor = expr; |
| | | 849 | | |
| | 4 | 850 | | var attributedTypeNames = UnwrapAttributedExpressionChain(ref cursor); |
| | 4 | 851 | | _ = TryUnwrapConvertExpression(ref cursor, out variableType, out variableTypeName); |
| | 4 | 852 | | UnwrapRemainingAttributedExpressions(ref cursor); |
| | | 853 | | |
| | 4 | 854 | | if (cursor is not VariableExpressionAst v) |
| | | 855 | | { |
| | 0 | 856 | | return false; |
| | | 857 | | } |
| | | 858 | | |
| | 4 | 859 | | 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. |
| | 4 | 864 | | if (variableType is null && variableTypeName is null && attributedTypeNames.Count > 0 && |
| | 4 | 865 | | TryInferVariableTypeFromAttributes(attributedTypeNames, out var inferredType, out var inferredTypeName)) |
| | | 866 | | { |
| | 4 | 867 | | variableType = inferredType; |
| | 4 | 868 | | variableTypeName = inferredTypeName; |
| | | 869 | | } |
| | | 870 | | |
| | 4 | 871 | | 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 | | { |
| | 4 | 881 | | var attributedTypeNames = new List<string>(); |
| | | 882 | | |
| | 12 | 883 | | while (cursor is AttributedExpressionAst aex) |
| | | 884 | | { |
| | 8 | 885 | | if (!string.IsNullOrWhiteSpace(aex.Attribute?.TypeName?.FullName)) |
| | | 886 | | { |
| | 8 | 887 | | attributedTypeNames.Add(aex.Attribute.TypeName.FullName); |
| | | 888 | | } |
| | | 889 | | |
| | 8 | 890 | | cursor = aex.Child; |
| | 8 | 891 | | } |
| | | 892 | | |
| | 4 | 893 | | 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 | | { |
| | 4 | 905 | | if (cursor is ConvertExpressionAst cex) |
| | | 906 | | { |
| | 0 | 907 | | variableTypeName = cex.Type.TypeName.FullName; |
| | 0 | 908 | | variableType = ResolvePowerShellTypeName(variableTypeName); |
| | 0 | 909 | | cursor = cex.Child; |
| | 0 | 910 | | return true; |
| | | 911 | | } |
| | | 912 | | |
| | 4 | 913 | | variableType = null; |
| | 4 | 914 | | variableTypeName = null; |
| | 4 | 915 | | 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 | | { |
| | 4 | 924 | | while (cursor is AttributedExpressionAst aex) |
| | | 925 | | { |
| | 0 | 926 | | cursor = aex.Child; |
| | 0 | 927 | | } |
| | 4 | 928 | | } |
| | | 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 | | { |
| | 8 | 939 | | for (var i = attributedTypeNames.Count - 1; i >= 0; i--) |
| | | 940 | | { |
| | 4 | 941 | | var tn = attributedTypeNames[i]; |
| | 4 | 942 | | var t = ResolvePowerShellTypeName(tn); |
| | 4 | 943 | | if (t is null) |
| | | 944 | | { |
| | | 945 | | continue; |
| | | 946 | | } |
| | | 947 | | |
| | 4 | 948 | | if (typeof(KestrunAnnotation).IsAssignableFrom(t)) |
| | | 949 | | { |
| | | 950 | | continue; |
| | | 951 | | } |
| | | 952 | | |
| | 4 | 953 | | variableType = t; |
| | 4 | 954 | | variableTypeName = tn; |
| | 4 | 955 | | return true; |
| | | 956 | | } |
| | | 957 | | |
| | 0 | 958 | | variableType = null; |
| | 0 | 959 | | variableTypeName = null; |
| | 0 | 960 | | 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 | | { |
| | 4 | 970 | | if (string.IsNullOrWhiteSpace(name)) |
| | | 971 | | { |
| | 0 | 972 | | 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 | | { |
| | 4 | 979 | | var psType = new PSTypeName(name.Trim()).Type; |
| | 4 | 980 | | if (psType is not null) |
| | | 981 | | { |
| | 4 | 982 | | return psType; |
| | | 983 | | } |
| | 0 | 984 | | } |
| | 0 | 985 | | catch |
| | | 986 | | { |
| | | 987 | | // Ignore and fall back to our heuristic mapping. |
| | 0 | 988 | | } |
| | | 989 | | |
| | | 990 | | // Common accelerators |
| | 0 | 991 | | var lowered = name.Trim(); |
| | 0 | 992 | | return lowered.ToLowerInvariant() switch |
| | 0 | 993 | | { |
| | 0 | 994 | | "int" => typeof(int), |
| | 0 | 995 | | "long" => typeof(long), |
| | 0 | 996 | | "double" => typeof(double), |
| | 0 | 997 | | "float" => typeof(float), |
| | 0 | 998 | | "decimal" => typeof(decimal), |
| | 0 | 999 | | "bool" => typeof(bool), |
| | 0 | 1000 | | "string" => typeof(string), |
| | 0 | 1001 | | "datetime" => typeof(DateTime), |
| | 0 | 1002 | | "guid" => typeof(Guid), |
| | 0 | 1003 | | "ipaddress" => typeof(System.Net.IPAddress), |
| | 0 | 1004 | | "hashtable" => typeof(Hashtable), |
| | 0 | 1005 | | "object" => typeof(object), |
| | 0 | 1006 | | _ => ResolveTypeFromName(NormalizePowerShellTypeName(lowered)) |
| | 0 | 1007 | | }; |
| | 4 | 1008 | | } |
| | | 1009 | | |
| | | 1010 | | private static string NormalizePowerShellTypeName(string name) |
| | | 1011 | | { |
| | | 1012 | | // Handle Nullable[T] |
| | 0 | 1013 | | if (name.StartsWith("nullable[", StringComparison.OrdinalIgnoreCase) && name.EndsWith(']')) |
| | | 1014 | | { |
| | 0 | 1015 | | var inner = name[9..^1]; |
| | 0 | 1016 | | var innerType = ResolvePowerShellTypeName(inner); |
| | 0 | 1017 | | if (innerType is not null && innerType.IsValueType) |
| | | 1018 | | { |
| | 0 | 1019 | | return typeof(Nullable<>).MakeGenericType(innerType).FullName!; |
| | | 1020 | | } |
| | | 1021 | | } |
| | | 1022 | | |
| | | 1023 | | // Handle array syntax: datetime[] |
| | 0 | 1024 | | if (name.EndsWith("[]", StringComparison.Ordinal)) |
| | | 1025 | | { |
| | 0 | 1026 | | var inner = name[..^2]; |
| | 0 | 1027 | | var innerType = ResolvePowerShellTypeName(inner); |
| | 0 | 1028 | | if (innerType is not null) |
| | | 1029 | | { |
| | 0 | 1030 | | return innerType.MakeArrayType().FullName!; |
| | | 1031 | | } |
| | | 1032 | | } |
| | | 1033 | | |
| | 0 | 1034 | | 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. |
| | 3 | 1040 | | var expr = statement switch |
| | 3 | 1041 | | { |
| | 1 | 1042 | | CommandExpressionAst ce => ce.Expression, |
| | 2 | 1043 | | PipelineAst p when p.PipelineElements is { Count: 1 } && p.PipelineElements[0] is CommandExpressionAst ce => |
| | 2 | 1044 | | _ => null |
| | 3 | 1045 | | }; |
| | | 1046 | | |
| | 3 | 1047 | | if (expr is null) |
| | | 1048 | | { |
| | 2 | 1049 | | var raw = statement.Extent.Text.Trim(); |
| | 2 | 1050 | | return (string.IsNullOrWhiteSpace(raw) ? null : raw, string.IsNullOrWhiteSpace(raw) ? null : raw); |
| | | 1051 | | } |
| | | 1052 | | |
| | 1 | 1053 | | var value = EvaluateArgumentExpression(expr); |
| | 1 | 1054 | | var text = expr.Extent.Text.Trim(); |
| | 1 | 1055 | | return (value, string.IsNullOrWhiteSpace(text) ? null : text); |
| | | 1056 | | } |
| | | 1057 | | |
| | | 1058 | | private static KestrunAnnotation? TryCreateAnnotation( |
| | | 1059 | | AttributeAst attr, |
| | | 1060 | | string defaultComponentName, |
| | | 1061 | | string componentNameArgument) |
| | | 1062 | | { |
| | 4 | 1063 | | var attribute = TryCreateKestrunAnnotation(attr, defaultComponentName, componentNameArgument); |
| | 4 | 1064 | | if (attribute is not null) |
| | | 1065 | | { |
| | 4 | 1066 | | return attribute; |
| | | 1067 | | } |
| | | 1068 | | |
| | 0 | 1069 | | attribute = TryCmdletMetadataAttribute(attr); |
| | | 1070 | | |
| | 0 | 1071 | | return attribute; |
| | | 1072 | | } |
| | | 1073 | | private static KestrunAnnotation? TryCreateKestrunAnnotation( |
| | | 1074 | | AttributeAst attr, |
| | | 1075 | | string defaultComponentName, |
| | | 1076 | | string componentNameArgument) |
| | | 1077 | | { |
| | 4 | 1078 | | var annotationType = ResolveKestrunAnnotationType(attr); |
| | 4 | 1079 | | if (annotationType is null) |
| | | 1080 | | { |
| | 0 | 1081 | | return null; |
| | | 1082 | | } |
| | 4 | 1083 | | var activatedInstance = Activator.CreateInstance(annotationType); |
| | | 1084 | | |
| | 4 | 1085 | | if (activatedInstance is not KestrunAnnotation instance) |
| | | 1086 | | { |
| | 0 | 1087 | | return null; |
| | | 1088 | | } |
| | 4 | 1089 | | if (activatedInstance is OpenApiExtensionAttribute) |
| | | 1090 | | { |
| | 0 | 1091 | | var par1 = attr.PositionalArguments.ElementAtOrDefault(0)?.Extent.Text.Trim('\''); |
| | 0 | 1092 | | var par2 = attr.PositionalArguments.ElementAtOrDefault(1)?.Extent.Text.Trim('\''); |
| | 0 | 1093 | | if (par1 is null || par2 is null) |
| | | 1094 | | { |
| | 0 | 1095 | | return null; |
| | | 1096 | | } |
| | | 1097 | | |
| | 0 | 1098 | | instance = new OpenApiExtensionAttribute(par1, par2); |
| | | 1099 | | } |
| | | 1100 | | else |
| | | 1101 | | { |
| | | 1102 | | // Apply named arguments as property setters. |
| | 24 | 1103 | | foreach (var na in attr.NamedArguments ?? Enumerable.Empty<NamedAttributeArgumentAst>()) |
| | | 1104 | | { |
| | 8 | 1105 | | 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. |
| | 4 | 1110 | | ApplyDefaultComponentName(instance, defaultComponentName, componentNameArgument); |
| | | 1111 | | |
| | 4 | 1112 | | 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 | | { |
| | 0 | 1122 | | if (expr is null) |
| | | 1123 | | { |
| | 0 | 1124 | | return null; |
| | | 1125 | | } |
| | | 1126 | | |
| | 0 | 1127 | | if (TryGetPlainConstantValue(expr, out var value)) |
| | | 1128 | | { |
| | 0 | 1129 | | return value; |
| | | 1130 | | } |
| | | 1131 | | |
| | 0 | 1132 | | if (TryGetPlainStringValue(expr, out value)) |
| | | 1133 | | { |
| | 0 | 1134 | | return value; |
| | | 1135 | | } |
| | | 1136 | | |
| | 0 | 1137 | | if (TryGetStaticTypeMemberValue(expr, out value)) |
| | | 1138 | | { |
| | 0 | 1139 | | return value; |
| | | 1140 | | } |
| | | 1141 | | // Could extend with more expression types as needed. |
| | 0 | 1142 | | 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 | | { |
| | 0 | 1153 | | if (expr is ConstantExpressionAst c) |
| | | 1154 | | { |
| | 0 | 1155 | | value = c.Value; |
| | 0 | 1156 | | return true; |
| | | 1157 | | } |
| | | 1158 | | |
| | 0 | 1159 | | value = null; |
| | 0 | 1160 | | 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 | | { |
| | 0 | 1171 | | if (expr is StringConstantExpressionAst s) |
| | | 1172 | | { |
| | 0 | 1173 | | value = s.Value; |
| | 0 | 1174 | | return true; |
| | | 1175 | | } |
| | | 1176 | | |
| | 0 | 1177 | | value = null; |
| | 0 | 1178 | | 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 | | { |
| | 0 | 1189 | | value = null; |
| | | 1190 | | |
| | 0 | 1191 | | if (expr is not MemberExpressionAst me || !me.Static || me.Expression is not TypeExpressionAst te) |
| | | 1192 | | { |
| | 0 | 1193 | | return false; |
| | | 1194 | | } |
| | | 1195 | | |
| | 0 | 1196 | | var targetType = te.TypeName.GetReflectionType(); // resolves [int] to System.Int32, etc. |
| | 0 | 1197 | | if (targetType is null) |
| | | 1198 | | { |
| | 0 | 1199 | | return false; |
| | | 1200 | | } |
| | | 1201 | | |
| | 0 | 1202 | | var memberName = GetMemberName(me.Member); |
| | 0 | 1203 | | if (string.IsNullOrWhiteSpace(memberName)) |
| | | 1204 | | { |
| | 0 | 1205 | | return false; |
| | | 1206 | | } |
| | | 1207 | | |
| | 0 | 1208 | | if (TryGetReflectedPropertyValue(targetType, memberName, out value)) |
| | | 1209 | | { |
| | 0 | 1210 | | return true; |
| | | 1211 | | } |
| | | 1212 | | |
| | 0 | 1213 | | if (TryGetReflectedFieldValue(targetType, memberName, out value)) |
| | | 1214 | | { |
| | 0 | 1215 | | return true; |
| | | 1216 | | } |
| | | 1217 | | |
| | | 1218 | | // Unsupported member type |
| | 0 | 1219 | | 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; |
| | 0 | 1232 | | var prop = targetType.GetProperty(memberName, flags); |
| | | 1233 | | |
| | 0 | 1234 | | if (prop is not null) |
| | | 1235 | | { |
| | 0 | 1236 | | value = prop.GetValue(null); |
| | 0 | 1237 | | return true; |
| | | 1238 | | } |
| | | 1239 | | |
| | 0 | 1240 | | value = null; |
| | 0 | 1241 | | 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; |
| | 0 | 1254 | | var field = targetType.GetField(memberName, flags); |
| | | 1255 | | |
| | 0 | 1256 | | if (field is not null) |
| | | 1257 | | { |
| | 0 | 1258 | | value = field.GetValue(null); |
| | 0 | 1259 | | return true; |
| | | 1260 | | } |
| | | 1261 | | |
| | 0 | 1262 | | value = null; |
| | 0 | 1263 | | 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) |
| | 0 | 1272 | | => (member as StringConstantExpressionAst)?.Value |
| | 0 | 1273 | | ?? (member as ConstantExpressionAst)?.Value?.ToString(); |
| | | 1274 | | |
| | | 1275 | | private static KestrunAnnotation? TryCmdletMetadataAttribute( |
| | | 1276 | | AttributeAst attr) |
| | | 1277 | | { |
| | 0 | 1278 | | var annotationType = ResolveCmdletMetadataAttributeType(attr); |
| | 0 | 1279 | | if (annotationType is null) |
| | | 1280 | | { |
| | 0 | 1281 | | return null; |
| | | 1282 | | } |
| | | 1283 | | |
| | 0 | 1284 | | var instance = new InternalPowershellAttribute(); |
| | 0 | 1285 | | switch (annotationType.Name) // <-- no GetType().Name |
| | | 1286 | | { |
| | | 1287 | | case nameof(ValidateRangeAttribute): |
| | | 1288 | | { |
| | 0 | 1289 | | var minObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(0)); |
| | 0 | 1290 | | var maxObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(1)); |
| | | 1291 | | |
| | 0 | 1292 | | instance.MinRange = minObj?.ToString(); |
| | 0 | 1293 | | instance.MaxRange = maxObj?.ToString(); |
| | 0 | 1294 | | break; |
| | | 1295 | | } |
| | | 1296 | | |
| | | 1297 | | case nameof(ValidateLengthAttribute): |
| | | 1298 | | { |
| | 0 | 1299 | | var minObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(0)); |
| | 0 | 1300 | | var maxObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(1)); |
| | 0 | 1301 | | if (int.TryParse(minObj?.ToString(), out var minLength)) |
| | | 1302 | | { |
| | 0 | 1303 | | instance.MinLength = minLength; |
| | | 1304 | | } |
| | | 1305 | | |
| | 0 | 1306 | | if (int.TryParse(maxObj?.ToString(), out var maxLength)) |
| | | 1307 | | { |
| | 0 | 1308 | | instance.MaxLength = maxLength; |
| | | 1309 | | } |
| | | 1310 | | |
| | 0 | 1311 | | break; |
| | | 1312 | | } |
| | | 1313 | | case nameof(ValidateSetAttribute): |
| | | 1314 | | { |
| | | 1315 | | // PowerShell: [ValidateSet('a','b')] is positional arguments |
| | 0 | 1316 | | instance.AllowedValues = [.. attr.PositionalArguments |
| | 0 | 1317 | | .Select(a => TryGetConstantLikeValue(a)?.ToString() ?? string.Empty) |
| | 0 | 1318 | | .Where(s => !string.IsNullOrEmpty(s))]; |
| | 0 | 1319 | | break; |
| | | 1320 | | } |
| | | 1321 | | |
| | | 1322 | | case nameof(ValidatePatternAttribute): |
| | | 1323 | | { |
| | | 1324 | | // PowerShell: [ValidatePattern('regex')] |
| | 0 | 1325 | | var patternObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(0)); |
| | 0 | 1326 | | instance.RegexPattern = patternObj?.ToString(); |
| | 0 | 1327 | | break; |
| | | 1328 | | } |
| | | 1329 | | |
| | | 1330 | | case nameof(ValidateCountAttribute): |
| | | 1331 | | { |
| | | 1332 | | // PowerShell: [ValidateCount(min, max)] |
| | 0 | 1333 | | var minObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(0)); |
| | 0 | 1334 | | var maxObj = TryGetConstantLikeValue(attr.PositionalArguments.ElementAtOrDefault(1)); |
| | | 1335 | | |
| | 0 | 1336 | | if (int.TryParse(minObj?.ToString(), out var minCount)) |
| | | 1337 | | { |
| | 0 | 1338 | | instance.MinItems = minCount; |
| | | 1339 | | } |
| | | 1340 | | |
| | 0 | 1341 | | if (int.TryParse(maxObj?.ToString(), out var maxCount)) |
| | | 1342 | | { |
| | 0 | 1343 | | instance.MaxItems = maxCount; |
| | | 1344 | | } |
| | | 1345 | | |
| | 0 | 1346 | | break; |
| | | 1347 | | } |
| | | 1348 | | |
| | | 1349 | | case nameof(ValidateNotNullOrEmptyAttribute): |
| | 0 | 1350 | | instance.ValidateNotNullOrEmptyAttribute = true; |
| | 0 | 1351 | | break; |
| | | 1352 | | |
| | | 1353 | | case nameof(ValidateNotNullAttribute): |
| | 0 | 1354 | | instance.ValidateNotNullAttribute = true; |
| | 0 | 1355 | | break; |
| | | 1356 | | |
| | | 1357 | | case nameof(ValidateNotNullOrWhiteSpaceAttribute): |
| | 0 | 1358 | | instance.ValidateNotNullOrWhiteSpaceAttribute = true; |
| | | 1359 | | break; |
| | | 1360 | | } |
| | | 1361 | | |
| | 0 | 1362 | | return instance; |
| | | 1363 | | } |
| | | 1364 | | private static void ApplyDefaultComponentName(Attribute annotation, string defaultComponentName, string componentNam |
| | | 1365 | | { |
| | 4 | 1366 | | if (string.IsNullOrWhiteSpace(defaultComponentName)) |
| | | 1367 | | { |
| | 0 | 1368 | | return; |
| | | 1369 | | } |
| | | 1370 | | |
| | 4 | 1371 | | var t = annotation.GetType(); |
| | 4 | 1372 | | var prop = t.GetProperty(componentNameArgument, BindingFlags.Instance | BindingFlags.Public | BindingFlags.Ignor |
| | 4 | 1373 | | if (prop is null || prop.PropertyType != typeof(string) || !prop.CanWrite) |
| | | 1374 | | { |
| | 4 | 1375 | | return; |
| | | 1376 | | } |
| | | 1377 | | |
| | 0 | 1378 | | var current = prop.GetValue(annotation) as string; |
| | 0 | 1379 | | if (!string.IsNullOrWhiteSpace(current)) |
| | | 1380 | | { |
| | 0 | 1381 | | return; |
| | | 1382 | | } |
| | | 1383 | | |
| | 0 | 1384 | | prop.SetValue(annotation, defaultComponentName); |
| | 0 | 1385 | | } |
| | | 1386 | | |
| | | 1387 | | private static void ApplyNamedArgument(KestrunAnnotation annotation, NamedAttributeArgumentAst na) |
| | | 1388 | | { |
| | 8 | 1389 | | var t = annotation.GetType(); |
| | 8 | 1390 | | var prop = t.GetProperty(na.ArgumentName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase) |
| | 8 | 1391 | | if (prop is null || !prop.CanWrite) |
| | | 1392 | | { |
| | 0 | 1393 | | return; |
| | | 1394 | | } |
| | | 1395 | | |
| | 8 | 1396 | | var raw = EvaluateArgumentExpression(na.Argument); |
| | 8 | 1397 | | var converted = ConvertToPropertyType(raw, prop.PropertyType); |
| | | 1398 | | try |
| | | 1399 | | { |
| | | 1400 | | // Avoid failing configuration due to a best-effort scan conversion. |
| | 8 | 1401 | | if (converted is not null) |
| | | 1402 | | { |
| | 8 | 1403 | | var targetType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; |
| | 8 | 1404 | | if (!targetType.IsInstanceOfType(converted) && !targetType.IsAssignableFrom(converted.GetType())) |
| | | 1405 | | { |
| | 0 | 1406 | | return; |
| | | 1407 | | } |
| | | 1408 | | } |
| | | 1409 | | |
| | 8 | 1410 | | prop.SetValue(annotation, converted); |
| | 8 | 1411 | | } |
| | 0 | 1412 | | catch (Exception ex) |
| | | 1413 | | { |
| | | 1414 | | // Log and continue on invalid property sets during best-effort scan. |
| | 0 | 1415 | | Serilog.Log.Warning( |
| | 0 | 1416 | | ex, |
| | 0 | 1417 | | "Failed to set property '{PropertyName}' on annotation '{AnnotationType}' with value expression '{ValueE |
| | 0 | 1418 | | na.ArgumentName, |
| | 0 | 1419 | | annotation.GetType().Name, |
| | 0 | 1420 | | na.Argument); |
| | 0 | 1421 | | } |
| | 8 | 1422 | | } |
| | | 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> |
| | 12 | 1429 | | private static object? EvaluateArgumentExpression(ExpressionAst expr) => expr switch |
| | 12 | 1430 | | { |
| | 1 | 1431 | | ArrayLiteralAst a => a.Elements.Select(EvaluateArgumentExpression).ToArray(), |
| | 1 | 1432 | | ParenExpressionAst p => EvaluateParenExpression(p), |
| | 9 | 1433 | | StringConstantExpressionAst s => s.Value, |
| | 0 | 1434 | | ExpandableStringExpressionAst e => e.Value, |
| | 1 | 1435 | | ConstantExpressionAst c => c.Value, |
| | 0 | 1436 | | VariableExpressionAst v => EvaluateVariableExpression(v), |
| | 0 | 1437 | | MemberExpressionAst me => TryEvaluateMemberExpression(me) ?? me.Extent.Text.Trim(), |
| | 0 | 1438 | | TypeExpressionAst te => ResolveTypeFromName(te.TypeName.FullName) is { } t ? t : te.TypeName.FullName, |
| | 0 | 1439 | | _ => expr.Extent.Text.Trim() |
| | 12 | 1440 | | }; |
| | | 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. |
| | 1 | 1451 | | if (p.Pipeline is PipelineAst pipeline && pipeline.PipelineElements is { Count: 1 } elems && elems[0] is Command |
| | | 1452 | | { |
| | 1 | 1453 | | return EvaluateArgumentExpression(ce.Expression); |
| | | 1454 | | } |
| | | 1455 | | // Fallback: return the raw text inside the parentheses. |
| | 0 | 1456 | | 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 | | { |
| | 0 | 1466 | | var name = v.VariablePath.UserPath; |
| | 0 | 1467 | | if (string.Equals(name, "null", StringComparison.OrdinalIgnoreCase)) |
| | | 1468 | | { |
| | 0 | 1469 | | return null; |
| | | 1470 | | } |
| | 0 | 1471 | | if (string.Equals(name, "true", StringComparison.OrdinalIgnoreCase)) |
| | | 1472 | | { |
| | 0 | 1473 | | return true; |
| | | 1474 | | } |
| | 0 | 1475 | | if (string.Equals(name, "false", StringComparison.OrdinalIgnoreCase)) |
| | | 1476 | | { |
| | 0 | 1477 | | return false; |
| | | 1478 | | } |
| | | 1479 | | |
| | | 1480 | | // Unknown variable, preserve source text (e.g. $foo) |
| | 0 | 1481 | | return v.Extent.Text.Trim(); |
| | | 1482 | | } |
| | | 1483 | | |
| | | 1484 | | private static object? TryEvaluateMemberExpression(MemberExpressionAst me) |
| | | 1485 | | { |
| | | 1486 | | // Common pattern in scripts: [SomeType]::Member |
| | 0 | 1487 | | if (me.Expression is not TypeExpressionAst te) |
| | | 1488 | | { |
| | 0 | 1489 | | return null; |
| | | 1490 | | } |
| | | 1491 | | |
| | 0 | 1492 | | var type = ResolveTypeFromName(te.TypeName.FullName); |
| | 0 | 1493 | | if (type is null) |
| | | 1494 | | { |
| | 0 | 1495 | | return null; |
| | | 1496 | | } |
| | | 1497 | | |
| | 0 | 1498 | | var memberName = me.Member.Extent.Text.Trim().Trim('"', '\''); |
| | 0 | 1499 | | if (type.IsEnum) |
| | | 1500 | | { |
| | | 1501 | | try |
| | | 1502 | | { |
| | 0 | 1503 | | return Enum.Parse(type, memberName, ignoreCase: true); |
| | | 1504 | | } |
| | 0 | 1505 | | catch |
| | | 1506 | | { |
| | 0 | 1507 | | return null; |
| | | 1508 | | } |
| | | 1509 | | } |
| | | 1510 | | |
| | | 1511 | | const BindingFlags Flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase; |
| | 0 | 1512 | | var f = type.GetField(memberName, Flags); |
| | 0 | 1513 | | if (f is not null) |
| | | 1514 | | { |
| | 0 | 1515 | | return f.GetValue(null); |
| | | 1516 | | } |
| | 0 | 1517 | | var p = type.GetProperty(memberName, Flags); |
| | 0 | 1518 | | return p?.GetValue(null); |
| | 0 | 1519 | | } |
| | | 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 | | { |
| | 10 | 1529 | | if (raw is null) |
| | | 1530 | | { |
| | 0 | 1531 | | return null; |
| | | 1532 | | } |
| | | 1533 | | |
| | 10 | 1534 | | var targetType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; |
| | | 1535 | | |
| | | 1536 | | // Arrays (e.g. string[] ContentType) |
| | 10 | 1537 | | if (targetType.IsArray) |
| | | 1538 | | { |
| | 1 | 1539 | | return ConvertArrayValue(raw, targetType); |
| | | 1540 | | } |
| | | 1541 | | |
| | | 1542 | | // If already the right type |
| | 9 | 1543 | | if (targetType.IsInstanceOfType(raw)) |
| | | 1544 | | { |
| | 6 | 1545 | | return raw; |
| | | 1546 | | } |
| | | 1547 | | |
| | | 1548 | | // Enums |
| | 3 | 1549 | | if (targetType.IsEnum) |
| | | 1550 | | { |
| | 3 | 1551 | | return ConvertEnumValue(raw, targetType); |
| | | 1552 | | } |
| | | 1553 | | |
| | | 1554 | | // Boolean strings like "$true" / "$false" |
| | 0 | 1555 | | if (targetType == typeof(bool) && raw is string bs) |
| | | 1556 | | { |
| | 0 | 1557 | | var boolVal = TryParseBooleanString(bs); |
| | 0 | 1558 | | if (boolVal.HasValue) |
| | | 1559 | | { |
| | 0 | 1560 | | return boolVal.Value; |
| | | 1561 | | } |
| | | 1562 | | } |
| | | 1563 | | |
| | | 1564 | | // Fallback: ChangeType or keep raw on failure |
| | 0 | 1565 | | 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 | | { |
| | 1 | 1577 | | var elementType = arrayType.GetElementType() ?? typeof(object); |
| | | 1578 | | |
| | | 1579 | | // If already the right array type, keep it |
| | 1 | 1580 | | if (arrayType.IsInstanceOfType(raw)) |
| | | 1581 | | { |
| | 0 | 1582 | | return raw; |
| | | 1583 | | } |
| | | 1584 | | |
| | | 1585 | | // Parse string list for string[] scenarios |
| | 1 | 1586 | | if (raw is string s && elementType == typeof(string)) |
| | | 1587 | | { |
| | 0 | 1588 | | var parsed = TryParseStringList(s); |
| | 0 | 1589 | | if (parsed is not null) |
| | | 1590 | | { |
| | 0 | 1591 | | return parsed; |
| | | 1592 | | } |
| | | 1593 | | // Treat scalar string as single-element array |
| | 0 | 1594 | | return new[] { s }; |
| | | 1595 | | } |
| | | 1596 | | |
| | | 1597 | | // IEnumerable -> array (avoid string as IEnumerable<char>) |
| | 1 | 1598 | | if (raw is IEnumerable enumerable and not string) |
| | | 1599 | | { |
| | 1 | 1600 | | return ConvertEnumerableToArray(enumerable, elementType); |
| | | 1601 | | } |
| | | 1602 | | |
| | | 1603 | | // Scalar -> single-element array |
| | 0 | 1604 | | var single = ConvertToPropertyType(raw, elementType); |
| | 0 | 1605 | | var singleArr = Array.CreateInstance(elementType, 1); |
| | 0 | 1606 | | singleArr.SetValue(single, 0); |
| | 0 | 1607 | | 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 | | { |
| | 0 | 1618 | | var t = listText.Trim(); |
| | 0 | 1619 | | var hasParens = (t.StartsWith("@(", StringComparison.Ordinal) || t.StartsWith('(')) && t.EndsWith(')'); |
| | 0 | 1620 | | if (!hasParens || !t.Contains(',', StringComparison.Ordinal)) |
| | | 1621 | | { |
| | 0 | 1622 | | return null; |
| | | 1623 | | } |
| | | 1624 | | |
| | 0 | 1625 | | var inner = t.StartsWith("@(", StringComparison.Ordinal) ? t[2..^1] : t[1..^1]; |
| | 0 | 1626 | | var parts = inner |
| | 0 | 1627 | | .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) |
| | 0 | 1628 | | .Select(p => p.Trim().Trim('\'', '"')) |
| | 0 | 1629 | | .Where(p => !string.IsNullOrWhiteSpace(p)) |
| | 0 | 1630 | | .ToArray(); |
| | | 1631 | | |
| | 0 | 1632 | | 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 | | { |
| | 1 | 1643 | | var items = new List<object?>(); |
| | 6 | 1644 | | foreach (var item in enumerable) |
| | | 1645 | | { |
| | 2 | 1646 | | items.Add(item); |
| | | 1647 | | } |
| | | 1648 | | |
| | 1 | 1649 | | var arr = Array.CreateInstance(elementType, items.Count); |
| | 6 | 1650 | | for (var i = 0; i < items.Count; i++) |
| | | 1651 | | { |
| | 2 | 1652 | | var convertedItem = ConvertToPropertyType(items[i], elementType); |
| | 2 | 1653 | | arr.SetValue(convertedItem, i); |
| | | 1654 | | } |
| | 1 | 1655 | | 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 | | { |
| | 3 | 1666 | | return raw is string s |
| | 3 | 1667 | | ? Enum.Parse(enumType, s, ignoreCase: true) |
| | 3 | 1668 | | : 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 | | { |
| | 0 | 1679 | | if (string.Equals(text, "$true", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "true", StringCompar |
| | | 1680 | | { |
| | 0 | 1681 | | return true; |
| | | 1682 | | } |
| | 0 | 1683 | | if (string.Equals(text, "$false", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "false", StringComp |
| | | 1684 | | { |
| | 0 | 1685 | | return false; |
| | | 1686 | | } |
| | | 1687 | | // Unrecognized token |
| | 0 | 1688 | | 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 | | { |
| | 0 | 1701 | | return Convert.ChangeType(raw, targetType); |
| | | 1702 | | } |
| | 0 | 1703 | | catch |
| | | 1704 | | { |
| | | 1705 | | // Best-effort: keep raw rather than failing the scan. |
| | 0 | 1706 | | return raw; |
| | | 1707 | | } |
| | 0 | 1708 | | } |
| | | 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. |
| | 4 | 1718 | | var shortName = attr.TypeName.Name; |
| | | 1719 | | |
| | | 1720 | | // If a namespace-qualified name is present, try it directly. |
| | 4 | 1721 | | var fullName = attr.TypeName.FullName; |
| | | 1722 | | |
| | 4 | 1723 | | var type = ResolveTypeFromName(fullName); |
| | 4 | 1724 | | type ??= ResolveTypeFromName(shortName); |
| | 4 | 1725 | | if (type is null && !shortName.EndsWith("Attribute", StringComparison.OrdinalIgnoreCase)) |
| | | 1726 | | { |
| | 4 | 1727 | | type ??= ResolveTypeFromName(shortName + "Attribute"); |
| | | 1728 | | } |
| | | 1729 | | |
| | 4 | 1730 | | 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. |
| | 0 | 1741 | | var shortName = attr.TypeName.Name; |
| | | 1742 | | |
| | | 1743 | | // If a namespace-qualified name is present, try it directly. |
| | 0 | 1744 | | var fullName = attr.TypeName.FullName; |
| | | 1745 | | |
| | 0 | 1746 | | var type = ResolveTypeFromName(fullName); |
| | 0 | 1747 | | type ??= ResolveTypeFromName(shortName); |
| | 0 | 1748 | | if (type is null && !shortName.EndsWith("Attribute", StringComparison.OrdinalIgnoreCase)) |
| | | 1749 | | { |
| | 0 | 1750 | | type ??= ResolveTypeFromName(shortName + "Attribute"); |
| | | 1751 | | } |
| | | 1752 | | |
| | 0 | 1753 | | return typeof(CmdletMetadataAttribute).IsAssignableFrom(type) |
| | 0 | 1754 | | ? type : null; |
| | | 1755 | | } |
| | | 1756 | | |
| | | 1757 | | private static Type? ResolveTypeFromName(string name) |
| | | 1758 | | { |
| | 12 | 1759 | | if (string.IsNullOrWhiteSpace(name)) |
| | | 1760 | | { |
| | 0 | 1761 | | return null; |
| | | 1762 | | } |
| | | 1763 | | |
| | | 1764 | | // Try Type.GetType first (works for assembly-qualified names) |
| | 12 | 1765 | | var t = Type.GetType(name, throwOnError: false, ignoreCase: true); |
| | 12 | 1766 | | if (t is not null) |
| | | 1767 | | { |
| | 0 | 1768 | | return t; |
| | | 1769 | | } |
| | | 1770 | | |
| | | 1771 | | // Search loaded assemblies (best-effort) |
| | 4996 | 1772 | | foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) |
| | | 1773 | | { |
| | 2488 | 1774 | | t = asm.GetType(name, throwOnError: false, ignoreCase: true); |
| | 2488 | 1775 | | if (t is not null) |
| | | 1776 | | { |
| | 4 | 1777 | | return t; |
| | | 1778 | | } |
| | | 1779 | | |
| | | 1780 | | // Also allow matching by short type name. |
| | | 1781 | | try |
| | | 1782 | | { |
| | 416784 | 1783 | | t = asm.GetTypes().FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); |
| | 2476 | 1784 | | if (t is not null) |
| | | 1785 | | { |
| | 0 | 1786 | | return t; |
| | | 1787 | | } |
| | 2476 | 1788 | | } |
| | 8 | 1789 | | catch |
| | | 1790 | | { |
| | | 1791 | | // Some dynamic/reflection-only assemblies can throw on GetTypes(). Ignore. |
| | 8 | 1792 | | } |
| | | 1793 | | } |
| | | 1794 | | |
| | 8 | 1795 | | return null; |
| | 0 | 1796 | | } |
| | | 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 | | { |
| | 4 | 1806 | | if (filter is null || filter.Count == 0) |
| | | 1807 | | { |
| | 4 | 1808 | | return true; |
| | | 1809 | | } |
| | | 1810 | | |
| | | 1811 | | // Compare short type name: [ComponentParameter] or [Namespace.ComponentParameter] |
| | 0 | 1812 | | var shortName = attr.TypeName.Name; |
| | 0 | 1813 | | 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 | | { |
| | 1 | 1823 | | var t = statementText.Trim(); |
| | | 1824 | | |
| | | 1825 | | // Basic "looks like our DSL annotation" |
| | 1 | 1826 | | if (!t.StartsWith('[') || |
| | 1 | 1827 | | !t.EndsWith(']') || |
| | 1 | 1828 | | !t.Contains('(', StringComparison.Ordinal)) |
| | | 1829 | | { |
| | 1 | 1830 | | return []; |
| | | 1831 | | } |
| | | 1832 | | |
| | | 1833 | | // Parse in a context where attributes are legal (param block) |
| | 0 | 1834 | | var synthetic = "param(\n" + t + "\n[object]$__x\n)\n"; |
| | | 1835 | | |
| | 0 | 1836 | | var ast = Parser.ParseInput(synthetic, out _, out var errors); |
| | 0 | 1837 | | if (errors is { Length: > 0 }) |
| | | 1838 | | { |
| | 0 | 1839 | | return []; |
| | | 1840 | | } |
| | | 1841 | | |
| | 0 | 1842 | | var paramBlock = ast.Find(n => n is ParamBlockAst, searchNestedScriptBlocks: true) as ParamBlockAst; |
| | 0 | 1843 | | var firstParam = paramBlock?.Parameters?.FirstOrDefault(); |
| | 0 | 1844 | | return firstParam?.Attributes? |
| | 0 | 1845 | | .OfType<AttributeAst>() |
| | 0 | 1846 | | .ToArray() |
| | 0 | 1847 | | ?? []; |
| | | 1848 | | } |
| | | 1849 | | } |