Skip to content

Commit

Permalink
Introduce TypeResolver with member filter and name comparer (#951)
Browse files Browse the repository at this point in the history
* Support filtering interop members with predicate
* Support configuring custom property name comparer in interop
* Make TypeReference use TypeResolver resolution logic
  • Loading branch information
lahma committed Aug 29, 2021
1 parent 20dcf69 commit 71de331
Show file tree
Hide file tree
Showing 10 changed files with 517 additions and 306 deletions.
1 change: 1 addition & 0 deletions Jint.Tests/Jint.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<SignAssembly>true</SignAssembly>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<NoWarn>612</NoWarn>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Runtime\Scripts\*.*;Parser\Scripts\*.*" />
Expand Down
24 changes: 15 additions & 9 deletions Jint.Tests/Runtime/Domain/HiddenMembers.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
namespace Jint.Tests.Runtime.Domain
using System;

namespace Jint.Tests.Runtime.Domain
{
public class HiddenMembers
{
[Obsolete]
public string Field1 = "Field1";

public string Field2 = "Field2";

[Obsolete]
public string Member1 { get; set; } = "Member1";

public string Member2 { get; set; } = "Member2";
public string Method1()
{
return "Method1";
}
public string Method2()
{
return "Method2";
}

[Obsolete]
public string Method1() => "Method1";

public string Method2() => "Method2";
}
}
128 changes: 128 additions & 0 deletions Jint.Tests/Runtime/InteropTests.MemberAccess.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System;
using Jint.Native;
using Jint.Runtime.Interop;
using Jint.Tests.Runtime.Domain;
using Xunit;

namespace Jint.Tests.Runtime
{
public partial class InteropTests
{
[Fact]
public void ShouldHideSpecificMembers()
{
var engine = new Engine(options => options.SetMemberAccessor((e, target, member) =>
{
if (target is HiddenMembers)
{
if (member == nameof(HiddenMembers.Member2) || member == nameof(HiddenMembers.Method2))
{
return JsValue.Undefined;
}
}
return null;
}));

engine.SetValue("m", new HiddenMembers());

Assert.Equal("Member1", engine.Evaluate("m.Member1").ToString());
Assert.Equal("undefined", engine.Evaluate("m.Member2").ToString());
Assert.Equal("Method1", engine.Evaluate("m.Method1()").ToString());
// check the method itself, not its invokation as it would mean invoking "undefined"
Assert.Equal("undefined", engine.Evaluate("m.Method2").ToString());
}

[Fact]
public void ShouldOverrideMembers()
{
var engine = new Engine(options => options.SetMemberAccessor((e, target, member) =>
{
if (target is HiddenMembers && member == nameof(HiddenMembers.Member1))
{
return "Orange";
}
return null;
}));

engine.SetValue("m", new HiddenMembers());

Assert.Equal("Orange", engine.Evaluate("m.Member1").ToString());
}

[Fact]
public void ShouldBeAbleToFilterMembers()
{
var engine = new Engine(options => options
.SetTypeResolver(new TypeResolver
{
MemberFilter = member => !Attribute.IsDefined(member, typeof(ObsoleteAttribute))
})
);

engine.SetValue("m", new HiddenMembers());

Assert.True(engine.Evaluate("m.Field1").IsUndefined());
Assert.True(engine.Evaluate("m.Member1").IsUndefined());
Assert.True(engine.Evaluate("m.Method1").IsUndefined());

Assert.True(engine.Evaluate("m.Field2").IsString());
Assert.True(engine.Evaluate("m.Member2").IsString());
Assert.True(engine.Evaluate("m.Method2()").IsString());
}

[Fact]
public void ShouldBeAbleToHideGetType()
{
var engine = new Engine(options => options
.SetTypeResolver(new TypeResolver
{
MemberFilter = member => !Attribute.IsDefined(member, typeof(ObsoleteAttribute))
})
);
engine.SetValue("m", new HiddenMembers());

Assert.True(engine.Evaluate("m.Method1").IsUndefined());

// reflection could bypass some safeguards
Assert.Equal("Method1", engine.Evaluate("m.GetType().GetMethod('Method1').Invoke(m, [])").AsString());

// but not when we forbid GetType
var hiddenGetTypeEngine = new Engine(options => options
.SetTypeResolver(new TypeResolver
{
MemberFilter = member => member.Name != nameof(GetType)
})
);
hiddenGetTypeEngine.SetValue("m", new HiddenMembers());
Assert.True(hiddenGetTypeEngine.Evaluate("m.GetType").IsUndefined());
}

[Fact]
public void TypeReferenceShouldUseTypeResolverConfiguration()
{
var engine = new Engine(options =>
{
options.SetTypeResolver(new TypeResolver
{
MemberFilter = member => !Attribute.IsDefined(member, typeof(ObsoleteAttribute))
});
});
engine.SetValue("EchoService", TypeReference.CreateTypeReference(engine, typeof(EchoService)));
Assert.Equal("anyone there", engine.Evaluate("EchoService.Echo('anyone there')").AsString());
Assert.Equal("anyone there", engine.Evaluate("EchoService.echo('anyone there')").AsString());
Assert.True(engine.Evaluate("EchoService.ECHO").IsUndefined());

Assert.True(engine.Evaluate("EchoService.Hidden").IsUndefined());
}

private static class EchoService
{
public static string Echo(string message) => message;

[Obsolete]
public static string Hidden(string message) => message;
}
}
}
80 changes: 36 additions & 44 deletions Jint.Tests/Runtime/InteropTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

namespace Jint.Tests.Runtime
{
public class InteropTests : IDisposable
public partial class InteropTests : IDisposable
{
private readonly Engine _engine;

Expand Down Expand Up @@ -2278,49 +2278,6 @@ public void ShouldBeAbleToJsonStringifyClrObjects()
Assert.Equal(jsValue, clrValue);
}

[Fact]
public void ShouldHideSpecificMembers()
{
var engine = new Engine(options => options.SetMemberAccessor((e, target, member) =>
{
if (target is HiddenMembers)
{
if (member == nameof(HiddenMembers.Member2) || member == nameof(HiddenMembers.Method2))
{
return JsValue.Undefined;
}
}
return null;
}));

engine.SetValue("m", new HiddenMembers());

Assert.Equal("Member1", engine.Evaluate("m.Member1").ToString());
Assert.Equal("undefined", engine.Evaluate("m.Member2").ToString());
Assert.Equal("Method1", engine.Evaluate("m.Method1()").ToString());
// check the method itself, not its invokation as it would mean invoking "undefined"
Assert.Equal("undefined", engine.Evaluate("m.Method2").ToString());
}

[Fact]
public void ShouldOverrideMembers()
{
var engine = new Engine(options => options.SetMemberAccessor((e, target, member) =>
{
if (target is HiddenMembers && member == nameof(HiddenMembers.Member1))
{
return "Orange";
}
return null;
}));

engine.SetValue("m", new HiddenMembers());

Assert.Equal("Orange", engine.Evaluate("m.Member1").ToString());
}

[Fact]
public void SettingValueViaIntegerIndexer()
{
Expand Down Expand Up @@ -2591,5 +2548,40 @@ public void ShouldHandleCyclicReferences()

Assert.Equal("Cyclic reference detected.", ex.Message);
}

[Fact]
public void CanConfigurePropertyNameMatcher()
{
// defaults
var e = new Engine();
e.SetValue("a", new A());
Assert.True(e.Evaluate("a.call1").IsObject());
Assert.True(e.Evaluate("a.Call1").IsObject());
Assert.True(e.Evaluate("a.CALL1").IsUndefined());

e = new Engine(options =>
{
options.SetTypeResolver(new TypeResolver
{
MemberNameComparer = StringComparer.Ordinal
});
});
e.SetValue("a", new A());
Assert.True(e.Evaluate("a.call1").IsUndefined());
Assert.True(e.Evaluate("a.Call1").IsObject());
Assert.True(e.Evaluate("a.CALL1").IsUndefined());

e = new Engine(options =>
{
options.SetTypeResolver(new TypeResolver
{
MemberNameComparer = StringComparer.OrdinalIgnoreCase
});
});
e.SetValue("a", new A());
Assert.True(e.Evaluate("a.call1").IsObject());
Assert.True(e.Evaluate("a.Call1").IsObject());
Assert.True(e.Evaluate("a.CALL1").IsObject());
}
}
}
3 changes: 0 additions & 3 deletions Jint/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
using Jint.Runtime.Descriptors;
using Jint.Runtime.Environments;
using Jint.Runtime.Interop;
using Jint.Runtime.Interop.Reflection;
using Jint.Runtime.Interpreter;
using Jint.Runtime.Interpreter.Expressions;
using Jint.Runtime.References;
Expand Down Expand Up @@ -82,8 +81,6 @@ public partial class Engine
internal readonly PropertyDescriptor _callerCalleeArgumentsThrowerConfigurable;
internal readonly PropertyDescriptor _callerCalleeArgumentsThrowerNonConfigurable;

internal static Dictionary<ClrPropertyDescriptorFactoriesKey, ReflectionAccessor> ReflectionAccessors = new();

internal readonly JintCallStack CallStack;

// needed in initial engine setup, for example CLR function construction
Expand Down
13 changes: 13 additions & 0 deletions Jint/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public sealed class Options
private List<Assembly> _lookupAssemblies = new();
private Predicate<Exception> _clrExceptionsHandler;
private IReferenceResolver _referenceResolver = DefaultReferenceResolver.Instance;
private TypeResolver _typeResolver = TypeResolver.Default;
private readonly List<Action<Engine>> _configurations = new();

private readonly List<Type> _extensionMethodClassTypes = new();
Expand Down Expand Up @@ -180,6 +181,16 @@ public Options SetTypeConverter(Func<Engine, ITypeConverter> typeConverterFactor
return this;
}

/// <summary>
/// Sets member name comparison strategy when finding CLR objects members.
/// By default member's first character casing is ignored and rest of the name is compared with strict equality.
/// </summary>
public Options SetTypeResolver(TypeResolver resolver)
{
_typeResolver = resolver;
return this;
}

/// <summary>
/// Registers a delegate that is called when CLR members are invoked. This allows
/// to change what values are returned for specific CLR objects, or if any value
Expand Down Expand Up @@ -365,7 +376,9 @@ internal void Apply(Engine engine)
internal List<IConstraint> _Constraints => _constraints;

internal Func<Engine, object, ObjectInstance> _WrapObjectHandler => _wrapObjectHandler;

internal MemberAccessorDelegate _MemberAccessor => _memberAccessor;
internal TypeResolver _TypeResolver => _typeResolver;

internal int MaxRecursionDepth => _maxRecursionDepth;

Expand Down

0 comments on commit 71de331

Please sign in to comment.