parsers/importParser.psm1
|
using module ..\models\bundlerConfig.psm1 using module ..\models\fileInfo.psm1 using module ..\helpers\astHelpers.psm1 using namespace System.Management.Automation.Language Class ImportParser { [BundlerConfig]$_config [AstHelpers]$_astHelper ImportParser ([BundlerConfig]$Config) { $this._config = $Config $this._astHelper = [AstHelpers]::new() } [hashtable[]]ParseFile([FileInfo]$file) { $result = @() $result += $this.ParseImportModule($file) $result += $this.ParseUsingModuleImports($file) $result += $this.ParseDotImports($file) $result += $this.ResolveAmpersandImports($file) return $result } # process "Import-Module" (like: Import-Module "file.psm1") [hashtable[]]ParseImportModule([FileInfo]$file) { $result = @() $commandAsts = $file.Ast.FindAll( { $args[0] -is [CommandAst] -and $args[0].CommandElements -and $args[0].CommandElements[0].Value -eq "Import-Module" }, $true) if (-not $commandAsts) { return $result } $type = "Module" foreach ($commandAst in $commandAsts) { $paths = $this.ParseImportModuleCommandAst($commandAst) foreach ($pathInfo in $paths) { $importPath = $this.ResolveImportPath($file, $type, $pathInfo.Path) if (-not $importPath) { continue } $result += @{ Path = $importPath PathAst = $pathInfo.Ast ImportAst = $commandAst Type = $type } } } return $result } # Get path properties from Import-Module CommandAst [hashtable[]]ParseImportModuleCommandAst([CommandAst]$commandAst) { $parameters = $this._astHelper.GetCommandAstParamsAst($commandAst) if (-not $parameters) { return @() } for ($i = 0; $i -lt $parameters.Count; $i++) { $param = $parameters[$i] if (($i -eq 0 -and -not $param.name) -or $param.name -eq "Name") { # first parameter without name or parameter -Name $paths = $this.ParseParameterValueAst($param.value) return $paths } } return @() } # get path value property from parameter value AST [hashtable[]]ParseParameterValueAst([ast]$parameter) { $result = @() $elements = @() if ($parameter -is [StringConstantExpressionAst] -or $parameter -is [ExpandableStringExpressionAst]) { $elements = @($parameter) } elseif ($parameter -is [ArrayLiteralAst] -and $parameter.Elements) { $elements = $parameter.Elements } else { return $result } foreach ($element in $elements) { if (($element -isnot [StringConstantExpressionAst] -and $element -isnot [ExpandableStringExpressionAst]) -or -not $this.IsStrIsPsFilePath($element.Value)) { continue } $result += @{ Path = $element.Value Ast = $element } } return $result } # TODO: implement .psd1 mainfest file parsing [bool]IsStrIsPsFilePath([string]$str) { return $str.EndsWith(".ps1") -or $str.EndsWith(".psm1") } # process "using module" (like: using module "file.psm1") [hashtable[]]ParseUsingModuleImports([FileInfo]$file) { $result = @() $usingStatements = $file.Ast.UsingStatements if (-not $usingStatements) { return $result } $type = "Using" foreach ($usingStatement in $usingStatements) { if ($usingStatement.UsingStatementKind -ne "Module") { continue } $importPath = $this.ResolveImportPath($file, $type, $usingStatement.Name.Value) if (-not $importPath) { continue } $result += @{ Path = $importPath PathAst = $usingStatement.Name ImportAst = $usingStatement Type = $type } } return $result } # process invocation imports, Dot and Ampersand (like: '& file.ps1' or '. file.ps1') [hashtable[]]ParseInvocationImports([FileInfo]$file, [string]$type) { $result = @() $commandAsts = $null if ($type -eq "Dot") { $commandAsts = $file.Ast.FindAll( { $args[0] -is [CommandAst] -and $args[0].InvocationOperator -eq 'Dot' }, $true) } elseif ($type -eq "Ampersand") { $commandAsts = $file.Ast.FindAll( { $args[0] -is [CommandAst] -and $args[0].InvocationOperator -eq 'Ampersand' }, $true) } else { return $result } if (-not $commandAsts) { return $result } foreach ($commandAst in $commandAsts) { if (-not $commandAst.CommandElements ` -or ($commandAst.CommandElements[0] -isnot [StringConstantExpressionAst] -and $commandAst.CommandElements[0] -isnot [ExpandableStringExpressionAst]) ` -or -not $commandAst.CommandElements[0].Value) { continue } $importPath = $this.ResolveImportPath($file, $type, $commandAst.CommandElements[0].Value) if (-not $importPath) { continue } $result += @{ Path = $importPath PathAst = $commandAst.CommandElements[0] ImportAst = $commandAst Type = $type } } return $result } # process "Dot commands" (like: . "file.ps1") [hashtable[]]ParseDotImports([FileInfo]$file) { return $this.ParseInvocationImports($file, "Dot") } # process "Ampersand commands" (like: & "file.ps1") [string[]]ResolveAmpersandImports([FileInfo]$file) { return $this.ParseInvocationImports($file, "Ampersand") } [string] ResolveImportPath( [FileInfo]$caller, # caller file info [string] $importType, # import kind ("dot", "ampersand", "module", "using") [string] $importPath # path string from the import statement ) { if (-not $importPath) { return $null } $callerPath = $caller.path $projectRoot = $this._config.projectRoot $resolved = $importPath $callerDir = [System.IO.Path]::GetDirectoryName($callerPath) $pathVars = @{ "PSScriptRoot" = $callerDir "PWD" = $projectRoot # emulate session path "HOME" = [Environment]::GetFolderPath('UserProfile') } # Expand ${PSScriptRoot} or $PSScriptRoot form first to avoid partial matches foreach ($key in $pathVars.Keys) { $value = $pathVars[$key] $resolved = $resolved -replace ("\$\{?$key\}?"), $value } # Tilde (~) expansion at the start of the path if ($resolved -match '^(~)([\\/]|$)') { $resolved = $resolved -replace '^~', $pathVars['HOME'] } # --- absolute? normalize and return ------------------------------------- if ([System.IO.Path]::IsPathRooted($resolved)) { return [System.IO.Path]::GetFullPath($resolved) } # --- choose base dir per import semantics (bundler rules) --------------- # dot, ampersand, module -> session PWD; in bundler we emulate it as ProjectRoot # using -> relative to the file where it's written $baseDir = switch ($importType) { 'using' { $callerDir } 'dot' { $projectRoot } 'ampersand' { $projectRoot } 'module' { $projectRoot } } # --- combine and normalize ---------------------------------------------- $combined = [System.IO.Path]::Combine($baseDir, $resolved) return [System.IO.Path]::GetFullPath($combined) } # TODO: Remove [string]ResolvePath_old([string]$ImportPath) { # Resolve script root path $baseDir = Split-Path -Path $this.path -Parent # Create a map with environment variables $context = @{ PSScriptRoot = $baseDir PWD = (Get-Location).Path HOME = [Environment]::GetFolderPath('UserProfile') } # Substitute variables like $PSScriptRoot, $HOME and so on $resolved = $ImportPath foreach ($key in $context.Keys) { $resolved = $resolved -replace ("\$" + $key), [regex]::Escape($context[$key]) } # Convert relative path to absolute if (-not [System.IO.Path]::IsPathRooted($resolved)) { $resolved = Join-Path -Path $baseDir -ChildPath $resolved } # Dots and slashes expansion $resolved = [System.IO.Path]::GetFullPath((Resolve-Path -LiteralPath $resolved -ErrorAction SilentlyContinue).Path) return $resolved } } |