Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce TypeResolver with member filter and name comparer #951

Merged
merged 4 commits into from
Aug 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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