You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Classes: after the compiled-script cache is cleared (> 1024 scripts), Import-Module -Force creates new class types, but the TypeCache keeps the old ones, so generic types of the same class no longer match #28122
A module has a class in a dot-sourced Class.ps1 and functions in a dot-sourced Functions.ps1. A function parameter has the type [System.Collections.Generic.List[Item]].
No file changes during the test. Save this as repro.ps1 and run pwsh -NoProfile -File repro.ps1:
param([int] $Count=1100)
# A two-file module: one class file and one function file, dot-sourced by the .psm1.$dir=Join-Path ([IO.Path]::GetTempPath()) "ClassRepro-$PID"New-Item-ItemType Directory -Force $dir|Out-NullSet-Content"$dir/Class.ps1"'class Item { [string] $Name }'Set-Content"$dir/Functions.ps1"@'function Add-Item { param([System.Collections.Generic.List[Item]] $List, [string] $Name) $List.Add([Item]@{ Name = $Name })}function New-ItemList { $list = [System.Collections.Generic.List[Item]]::new() Add-Item -List $list -Name 'a' $list}'@Set-Content"$dir/ClassRepro.psm1"@'. $PSScriptRoot/Class.ps1. $PSScriptRoot/Functions.ps1Export-ModuleMember -Function Add-Item, New-ItemList'@Import-Module"$dir/ClassRepro.psm1"-Force
$old=& (Get-Module ClassRepro) { [Item] }
"1st import: New-ItemList -> $((New-ItemList).Count) item(s)"# Compile more than 1024 distinct scripts (a long test run does the same).for ($i=0; $i-lt$Count; $i++) { $null= [scriptblock]::Create("`$null = $i") }
Import-Module"$dir/ClassRepro.psm1"-Force
$new=& (Get-Module ClassRepro) { [Item] }
"[Item] after re-import is a new type: $($new-ne$old)"try { "2nd import: New-ItemList -> $((New-ItemList).Count) item(s)" }
catch { "2nd import: New-ItemList FAILED: $($_.Exception.Message)" }
try {
$list= [System.Collections.Generic.List`1].MakeGenericType($new)::new()
Add-Item-List $list-Name 'b'"2nd import: Add-Item with List[new Item] -> OK"
}
catch { "2nd import: Add-Item with List[new Item] FAILED: $($_.Exception.Message)" }
Remove-Item-Recurse -Force $dir
Run it a second time with -Count 0 (no flood) for comparison.
What we found (mechanism)
Two process-wide caches get out of step:
CompiledScriptBlock.s_cachedScripts holds compiled scripts keyed by (path, content). When it
holds more than 1024 entries, it is cleared completely. While the entry for Class.ps1 is
cached, Import-Module -Force re-runs the same compiled script, and [Item] keeps its type.
After the clear, the re-import compiles Class.ps1 again, and a second PowerShell Class Assembly (Version 1.0.0.2) defines a new Item.
System.Management.Automation.Language.TypeCache.s_cache maps (type name,
TypeResolutionState) to a resolved type. Nothing clears it. It still maps Item and System.Collections.Generic.List[Item] to the 1.0.0.1 types. Functions.ps1 is compiled again
after the re-import, and its [List[Item]] resolves through this cache to List<Item v1>. [Item]@{...} resolves to the new Item v2. The two do not fit.
Evidence:
After the re-import, the -List parameter type of Add-Item has the OLD Item as its generic
argument. With -Count 0, it has the only Item.
If the TypeCache entries whose name contains Item are removed (by reflection) before the
re-import, the repro passes: the parameter type then uses the new Item.
If the function is in the .psm1 itself instead of a dot-sourced file, the repro passes.
The documentation says (about_Classes, Limitations): "PowerShell classes can't be unloaded or
reloaded in a session. Workaround: Start a new session." and "there's no way to load any updated
classes." #20893 and #21151 were closed on that basis, because in those issues the user changed
or redefined the class.
We think this case goes beyond the documented limitation:
Nothing is reloaded or changed. The source is identical. The user does not ask for new class
types.
For identical source, Import-Module -Force normally keeps the class types (they are not
reloaded, as documented). This case breaks that normal behaviour: the class types change, and
PowerShell does not report it.
It is non-deterministic from the user's view. It depends on how many distinct scripts the
process compiled since the last import (here about 1024), a number that the user cannot see or
control. The same Import-Module -Force works early in a session and fails late in it.
The result is a split, not a clean reload: in the same compiled script, [Item] is the new
type and [List[Item]] is the old type. No documented behaviour gives two different Item
types in one statement.
It occurs in normal use: a large Pester run re-imports the module under test in many test
files (Import-Module -Force in BeforeAll) and compiles thousands of scriptblocks. The
failures then occur only in the late test files and only in full runs.
Possible fixes (for discussion):
When a script whose AST defines types is compiled again, remove from the TypeCache the entries
that point to types of the replaced dynamic assembly, or key those entries by the defining
assembly.
Or do not evict, from s_cachedScripts, entries whose scripts define types, so that the class
identity stays stable for identical source (the same as before the eviction).
At minimum, document that class identity depends on the script-block cache.
Workaround
Keep a reference to the compiled class scriptblocks at first import, and on each re-import run
those kept scriptblocks again instead of dot-sourcing the class files. Then the class types stay
the same for the life of the process.
(Tested on 7.6.5 only. #20893 shows the TypeCache half on 7.4.0.)
Expected behavior
The second `Import-Module -Force` behaves the same whether or not other scripts ran before it. With`-Count 0` this is the output:1st import: New-ItemList -> 1 item(s)[Item] after re-import is a new type: False2nd import: New-ItemList -> 1 item(s)2nd import: Add-Item with List[new Item] -> OKIf the re-import does make a new `[Item]` type, then `[List[Item]]` in code compiled after there-import must be built on that same new type.
Actual behavior
With the default `-Count 1100`:1st import: New-ItemList -> 1 item(s)[Item] after re-import is a new type: True2nd import: New-ItemList FAILED: Cannot find an overload for "Add" and the argument count: "1".2nd import: Add-Item with List[new Item] FAILED: Cannot process argument transformation on parameter 'List'. Cannot convert the "System.Collections.Generic.List`1[Item]" value of type "System.Collections.Generic.List`1[[Item, PowerShell Class Assembly, Version=1.0.0.2, Culture=neutral, PublicKeyToken=null]]" to type "Item".The class file did not change. The module source did not change. Only the number of scripts thatthe process compiled before the re-import is different. In a sweep, `-Count 1020` passes and`-Count 1030` fails.
Error details
System.Management.Automation.MethodException: Cannot find an overload for "Add" and the argument count: "1".At <temp>\ClassRepro-<pid>\Functions.ps1:3 char:5+ $List.Add([Item]@{ Name = $Name })+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~System.Management.Automation.ParameterBindingArgumentTransformationException: Cannot process argument transformation on parameter 'List'. Cannot convert the "System.Collections.Generic.List`1[Item]" value of type "System.Collections.Generic.List`1[[Item, PowerShell Class Assembly, Version=1.0.0.2, Culture=neutral, PublicKeyToken=null]]" to type "Item".
Environment data
Name Value
---------
PSVersion 7.6.5
PSEdition Core
GitCommitId 7.6.5
OS Microsoft Windows 10.0.26200
Platform Win32NT
PSCompatibleVersions {1.0,2.0,3.0,4.0…}
PSRemotingProtocolVersion 2.4
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
Prerequisites
Steps to reproduce
A module has a class in a dot-sourced
Class.ps1and functions in a dot-sourcedFunctions.ps1. A function parameter has the type[System.Collections.Generic.List[Item]].No file changes during the test. Save this as
repro.ps1and runpwsh -NoProfile -File repro.ps1:Run it a second time with
-Count 0(no flood) for comparison.What we found (mechanism)
Two process-wide caches get out of step:
CompiledScriptBlock.s_cachedScriptsholds compiled scripts keyed by (path, content). When itholds more than 1024 entries, it is cleared completely. While the entry for
Class.ps1iscached,
Import-Module -Forcere-runs the same compiled script, and[Item]keeps its type.After the clear, the re-import compiles
Class.ps1again, and a secondPowerShell Class Assembly(Version 1.0.0.2) defines a newItem.System.Management.Automation.Language.TypeCache.s_cachemaps (type name,TypeResolutionState) to a resolved type. Nothing clears it. It still maps
ItemandSystem.Collections.Generic.List[Item]to the 1.0.0.1 types.Functions.ps1is compiled againafter the re-import, and its
[List[Item]]resolves through this cache toList<Item v1>.[Item]@{...}resolves to the newItem v2. The two do not fit.Evidence:
-Listparameter type ofAdd-Itemhas the OLDItemas its genericargument. With
-Count 0, it has the onlyItem.Itemare removed (by reflection) before there-import, the repro passes: the parameter type then uses the new
Item..psm1itself instead of a dot-sourced file, the repro passes.classes in constructors vs generic type arguments #20893 shows the same TypeCache behaviour for a user redefinition: generic arguments resolve tothe first definition,
[User]to the newest one.Bug or by design?
The documentation says (about_Classes, Limitations): "PowerShell classes can't be unloaded or
reloaded in a session. Workaround: Start a new session." and "there's no way to load any updated
classes." #20893 and #21151 were closed on that basis, because in those issues the user changed
or redefined the class.
We think this case goes beyond the documented limitation:
types.
Import-Module -Forcenormally keeps the class types (they are notreloaded, as documented). This case breaks that normal behaviour: the class types change, and
PowerShell does not report it.
process compiled since the last import (here about 1024), a number that the user cannot see or
control. The same
Import-Module -Forceworks early in a session and fails late in it.[Item]is the newtype and
[List[Item]]is the old type. No documented behaviour gives two differentItemtypes in one statement.
files (
Import-Module -ForceinBeforeAll) and compiles thousands of scriptblocks. Thefailures then occur only in the late test files and only in full runs.
Possible fixes (for discussion):
that point to types of the replaced dynamic assembly, or key those entries by the defining
assembly.
s_cachedScripts, entries whose scripts define types, so that the classidentity stays stable for identical source (the same as before the eviction).
Workaround
Keep a reference to the compiled class scriptblocks at first import, and on each re-import run
those kept scriptblocks again instead of dot-sourcing the class files. Then the class types stay
the same for the life of the process.
(Tested on 7.6.5 only. #20893 shows the TypeCache half on 7.4.0.)
Expected behavior
Actual behavior
Error details
Environment data
Visuals
No response