Fix dynamic class assembly name (#5292)

Using the assembly name to hint at the source of the classes was
problematic in multiple ways.

This change stores the actual filename in an attribute on the assembly.

So for a given type, one can get the assembly this way:

[SomeType].Assembly.GetCustomAttributes() |
    ? { $_ -is [System.Management.Automation.DynamicClassImplementationAssemblyAttribute] } |
    % { $_.ScriptFile }
This commit is contained in:
Jason Shirk 2017-11-02 10:29:10 -07:00 committed by GitHub
parent 237ccbdf6d
commit 71d5439bbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 42 additions and 24 deletions

View File

@ -560,6 +560,10 @@ namespace System.Management.Automation
[AttributeUsage(AttributeTargets.Assembly)]
public class DynamicClassImplementationAssemblyAttribute : Attribute
{
/// <summary>
/// The (possibly null) path to the file defining this class.
/// </summary>
public string ScriptFile { get; set; }
}
#endregion Misc Attributes

View File

@ -16,10 +16,11 @@ namespace System.Management.Automation.Language
{
internal class TypeDefiner
{
internal const string DynamicClassAssemblyName = "PowerShell Class Assembly";
private static int s_globalCounter = 0;
private static readonly object[] s_emptyArgArray = Utils.EmptyArray<object>();
private static readonly CustomAttributeBuilder s_hiddenCustomAttributeBuilder =
new CustomAttributeBuilder(typeof(HiddenAttribute).GetConstructor(Type.EmptyTypes), s_emptyArgArray);
new CustomAttributeBuilder(typeof(HiddenAttribute).GetConstructor(Type.EmptyTypes), Utils.EmptyArray<object>());
private static readonly string s_sessionStateKeeperFieldName = "__sessionStateKeeper";
internal static readonly string SessionStateFieldName = "__sessionState";
@ -1100,30 +1101,40 @@ namespace System.Management.Automation.Language
}
}
private static IEnumerable<CustomAttributeBuilder> GetAssemblyAttributeBuilders()
private static IEnumerable<CustomAttributeBuilder> GetAssemblyAttributeBuilders(string scriptFile)
{
yield return new CustomAttributeBuilder(typeof(DynamicClassImplementationAssemblyAttribute).GetConstructor(Type.EmptyTypes), s_emptyArgArray);
var ctor = typeof(DynamicClassImplementationAssemblyAttribute).GetConstructor(Type.EmptyTypes);
var emptyArgs = Utils.EmptyArray<object>();
if (string.IsNullOrEmpty(scriptFile)) {
yield return new CustomAttributeBuilder(ctor, emptyArgs);
yield break;
}
var propertyInfo = new PropertyInfo[] {
typeof(DynamicClassImplementationAssemblyAttribute).GetProperty(nameof(DynamicClassImplementationAssemblyAttribute.ScriptFile)) };
var propertyArgs = new object[] { scriptFile };
yield return new CustomAttributeBuilder(ctor, emptyArgs,
propertyInfo, propertyArgs, Utils.EmptyArray<FieldInfo>(), emptyArgs);
}
private static int counter = 0;
internal static Assembly DefineTypes(Parser parser, Ast rootAst, TypeDefinitionAst[] typeDefinitions)
{
Diagnostics.Assert(rootAst.Parent == null, "Caller should only define types from the root ast");
var definedTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// First character is a special mark that allows us to cheaply ignore dynamic generated assemblies in ClrFacade.GetAssemblies()
// The replaces at the end are for not-allowed characters. They are replaced by similar-looking chars.
string assemblyName = ClrFacade.FIRST_CHAR_PSASSEMBLY_MARK + (string.IsNullOrWhiteSpace(rootAst.Extent.File)
? "powershell"
: rootAst.Extent.File
.Replace('\\', (char)0x29f9)
.Replace('/', (char)0x29f9)
.Replace(',', (char)0x201a)
.Replace(':', (char)0x0589));
var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(assemblyName),
AssemblyBuilderAccess.RunAndCollect, GetAssemblyAttributeBuilders());
var module = assembly.DefineDynamicModule(assemblyName);
var assemblyName = new AssemblyName(DynamicClassAssemblyName)
{
// We could generate a unique name, but a unique version works too.
Version = new Version(1, 0, 0, Interlocked.Increment(ref counter))
};
var assembly = AssemblyBuilder.DefineDynamicAssembly(assemblyName,
AssemblyBuilderAccess.RunAndCollect, GetAssemblyAttributeBuilders(rootAst.Extent.File));
var module = assembly.DefineDynamicModule(DynamicClassAssemblyName);
var defineTypeHelpers = new List<DefineTypeHelper>();
var defineEnumHelpers = new List<DefineEnumHelper>();

View File

@ -41,12 +41,6 @@ namespace System.Management.Automation
}
}
/// <summary>
/// We need it to avoid calling lookups inside dynamic assemblies with PS Types, so we exclude it from GetAssemblies().
/// We use this convention for names to archive it.
/// </summary>
internal static readonly char FIRST_CHAR_PSASSEMBLY_MARK = (char)0x29f9;
#region Assembly
internal static IEnumerable<Assembly> GetAssemblies(TypeResolutionState typeResolutionState, TypeName typeName)
@ -65,7 +59,8 @@ namespace System.Management.Automation
internal static IEnumerable<Assembly> GetAssemblies(string namespaceQualifiedTypeName = null)
{
return PSAssemblyLoadContext.GetAssembly(namespaceQualifiedTypeName) ??
AppDomain.CurrentDomain.GetAssemblies().Where(a => !(a.FullName.Length > 0 && a.FullName[0] == FIRST_CHAR_PSASSEMBLY_MARK));
AppDomain.CurrentDomain.GetAssemblies().Where(a =>
!TypeDefiner.DynamicClassAssemblyName.Equals(a.GetName().Name, StringComparison.Ordinal));
}
/// <summary>

View File

@ -688,6 +688,14 @@ Describe 'Type building' -Tags "CI" {
++$a::a | Should Be 2
}
}
It 'should get the script from a class type' {
class C {}
$a = [C].Assembly.GetCustomAttributes($false).Where{
$_ -is [System.Management.Automation.DynamicClassImplementationAssemblyAttribute]}
$a.ScriptFile | Should BeExactly $PSCommandPath
}
}
Describe 'RuntimeType created for TypeDefinitionAst' -Tags "CI" {