Skip to content

Commit

Permalink
Merge branch 'main' into system.text.json
Browse files Browse the repository at this point in the history
  • Loading branch information
exyi committed Nov 2, 2024
2 parents 6020b0a + a546262 commit 592e1dc
Show file tree
Hide file tree
Showing 82 changed files with 1,871 additions and 239 deletions.
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- 'main'
- 'main-*'
- 'release/**'
pull_request:
workflow_dispatch:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,68 @@ public void CallSite()
VerifyCS.Diagnostic(UnsupportedCallSiteAttributeAnalyzer.DoNotInvokeMethodFromUnsupportedCallSite)
.WithLocation(0).WithArguments("Target", "due to: \"REASON\""));
}

[Fact]
public async Task Test_Warning_InvokeMethod_WithUnsupportedCallSiteAttribute_InLambda()
{
await VerifyCS.VerifyAnalyzerAsync("""
using System;
using System.IO;
using DotVVM.Framework.CodeAnalysis;
namespace ConsoleApplication1
{
public class RegularClass
{
[UnsupportedCallSite(CallSiteType.ServerSide)]
public void Target()
{
}
public void CallSite()
{
Action fn = () => {|#0:Target()|};
}
}
}
""",

VerifyCS.Diagnostic(UnsupportedCallSiteAttributeAnalyzer.DoNotInvokeMethodFromUnsupportedCallSite)
.WithLocation(0).WithArguments("Target", string.Empty)
);
}

[Fact]
public async Task Test_Warning_InvokeMethod_WithUnsupportedCallSiteAttribute_InLinqExpression()
{
// supressed in Linq.Expressions
await VerifyCS.VerifyAnalyzerAsync("""
using System;
using System.IO;
using System.Linq.Expressions;
using DotVVM.Framework.CodeAnalysis;
namespace ConsoleApplication1
{
public class RegularClass
{
[UnsupportedCallSite(CallSiteType.ServerSide)]
public int Target()
{
return 0;
}
public void CallSite()
{
Expression<Action> fn = () => Target();
Expression<Func<int, int>> fn2 = arg => Target();
}
}
}
""",
expected: []
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,36 @@ public class DefaultViewModel : DotvvmViewModelBase
await VerifyCS.VerifyAnalyzerAsync(text);
}

[Fact]
public async Task Test_IgnoreNonSerializedMembers_BindDirectionNoneInherited_ViewModel()
{
var text = @"
using DotVVM.Framework.ViewModel;
using Newtonsoft.Json;
using System;
using System.IO;
namespace ConsoleApplication1
{
public class BaseVM : DotvvmViewModelBase
{
[Bind(Direction.None)]
public virtual Stream Property1 { get; }
[JsonIgnore]
public virtual Stream Property2 { get; }
}
public class DefaultViewModel : BaseVM
{
public override Stream Property1 { get; }
public override Stream Property2 { get; }
}
}";

await VerifyCS.VerifyAnalyzerAsync(text, expected: []);
}

[Fact]
public async Task Test_SelfReferencingTypes_GenericArgs_ViewModel()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public sealed class UnsupportedCallSiteAttributeAnalyzer : DiagnosticAnalyzer
private static readonly LocalizableResourceString unsupportedCallSiteMessage = new(nameof(Resources.ApiUsage_UnsupportedCallSite_Message), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableResourceString unsupportedCallSiteDescription = new(nameof(Resources.ApiUsage_UnsupportedCallSite_Description), Resources.ResourceManager, typeof(Resources));
private const string unsupportedCallSiteAttributeMetadataName = "DotVVM.Framework.CodeAnalysis.UnsupportedCallSiteAttribute";
private const string linqExpressionsExpression1MetadataName = "System.Linq.Expressions.Expression`1";
private const int callSiteTypeServerUnderlyingValue = 0;

public static DiagnosticDescriptor DoNotInvokeMethodFromUnsupportedCallSite = new DiagnosticDescriptor(
Expand Down Expand Up @@ -48,6 +49,10 @@ public override void Initialize(AnalysisContext context)
if (attribute.ConstructorArguments.First().Value is not int callSiteType || callSiteTypeServerUnderlyingValue != callSiteType)
return;

if (context.Operation.IsWithinExpressionTree(context.Compilation.GetTypeByMetadataName(linqExpressionsExpression1MetadataName)))
// supress in Linq.Expression trees, such as in ValueOrBinding.Select
return;

var reason = (string?)attribute.ConstructorArguments.Skip(1).First().Value;
context.ReportDiagnostic(
Diagnostic.Create(
Expand Down
3 changes: 1 addition & 2 deletions src/Analyzers/Analyzers/DiagnosticCategory.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text;

namespace DotVVM.Analyzers
Expand Down
51 changes: 51 additions & 0 deletions src/Analyzers/Analyzers/RoslynTreeHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;

namespace DotVVM.Analyzers
{
internal static class RoslynTreeHelper
{
// from https://github.com/Evangelink/roslyn-analyzers/blob/48424637e03e48bbbd8e02862c940e7eb5436817/src/Utilities/Compiler/Extensions/IOperationExtensions.cs
private static readonly ImmutableArray<OperationKind> s_LambdaAndLocalFunctionKinds =
ImmutableArray.Create(OperationKind.AnonymousFunction, OperationKind.LocalFunction);

/// <summary>
/// Gets the first ancestor of this operation with:
/// 1. Any OperationKind from the specified <paramref name="ancestorKinds"/>.
/// 2. If <paramref name="predicate"/> is non-null, it succeeds for the ancestor.
/// Returns null if there is no such ancestor.
/// </summary>
public static IOperation? GetAncestor(this IOperation root, ImmutableArray<OperationKind> ancestorKinds, Func<IOperation, bool>? predicate = null)
{
if (root == null)
{
throw new ArgumentNullException(nameof(root));
}

var ancestor = root;
do
{
ancestor = ancestor.Parent;
} while (ancestor != null && !ancestorKinds.Contains(ancestor.Kind));

if (ancestor != null)
{
if (predicate != null && !predicate(ancestor))
{
return GetAncestor(ancestor, ancestorKinds, predicate);
}
return ancestor;
}
else
{
return default;
}
}

public static bool IsWithinExpressionTree(this IOperation operation, INamedTypeSymbol? linqExpressionTreeType)
=> linqExpressionTreeType != null
&& operation.GetAncestor(s_LambdaAndLocalFunctionKinds)?.Parent?.Type?.OriginalDefinition is { } lambdaType
&& SymbolEqualityComparer.Default.Equals(linqExpressionTreeType, lambdaType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,11 @@ private static bool IsSerializationOverriden(IPropertySymbol property, Serializa
return false;
}

/// <summary> Returns true if the property has a Bind(Direction.None) or JsonIgnore attribute </summary>
private static bool IsSerializationIgnored(IPropertySymbol property, SerializabilityAnalysisContext context)
{
return IsSerializationIgnoredUsingBindAttribute(property, context) || IsSerializationIgnoredUsingJsonIgnoreAttribute(property, context);
return IsSerializationIgnoredUsingBindAttribute(property, context) || IsSerializationIgnoredUsingJsonIgnoreAttribute(property, context) ||
property.OverriddenProperty is {} overriddenProperty && IsSerializationIgnored(overriddenProperty, context);
}

private static bool IsSerializationIgnoredUsingBindAttribute(ISymbol propertyOrFieldSymbol, SerializabilityAnalysisContext context)
Expand Down
6 changes: 6 additions & 0 deletions src/Framework/Core/Storage/UploadedFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@ namespace DotVVM.Core.Storage
{
public class UploadedFile
{
/// <summary> A unique, randomly generated ID of the uploaded file. Use this ID to get the file from <see cref="IUploadedFileStorage.GetFileAsync(Guid)" /> </summary>
public Guid FileId { get; set; }

/// <summary> A user-specified name of the file. Use with caution, the user may specify this to be any string (for example <c>../../Web.config</c>). </summary>
public string? FileName { get; set; }

/// <summary> Length of the file in bytes. Use with caution, the user may manipulate with this property and it might not correspond to the file returned from <see cref="IUploadedFileStorage" />. </summary>
public FileSize FileSize { get; set; } = new FileSize();

/// <summary> If the file type matched one of type MIME types or extensions in <c>FileUpload.AllowedFileTypes</c>. Use with caution, the user may manipulate with this property. </summary>
public bool IsFileTypeAllowed { get; set; } = true;

/// <summary> If the file size is larger that the limit specified in <c>FileUpload.MaxFileSize</c>. Use with caution, the user may manipulate with this property. </summary>
public bool IsMaxSizeExceeded { get; set; } = false;

/// <summary> If the file satisfies both allowed file types and the size limit. Use with caution, the user may manipulate with this property. </summary>
public bool IsAllowed
=> IsFileTypeAllowed && !IsMaxSizeExceeded;
}
Expand Down
6 changes: 3 additions & 3 deletions src/Framework/Framework/Binding/BindingHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ public static string FormatKnockoutScript(this ParametrizedCode code, DotvvmBind
/// Gets Internal.PathFragmentProperty or DataContext.KnockoutExpression. Returns null if none of these is set.
/// </summary>
public static string? GetDataContextPathFragment(this DotvvmBindableObject currentControl) =>
(string?)currentControl.GetValue(Internal.PathFragmentProperty, inherit: false) ??
(currentControl.GetBinding(DotvvmBindableObject.DataContextProperty, inherit: false) is IValueBinding binding ?
currentControl.properties.TryGet(Internal.PathFragmentProperty, out var pathFragment) && pathFragment is string pathFragmentStr ? pathFragmentStr :
currentControl.properties.TryGet(DotvvmBindableObject.DataContextProperty, out var dataContext) && dataContext is IValueBinding binding ?
binding.GetProperty<SimplePathExpressionBindingProperty>()
.Code.FormatKnockoutScript(currentControl, binding) :
null);
null;


// PERF: maybe safe last GetValue's target/binding to ThreadLocal variable, so the path does not have to be traversed twice
Expand Down
44 changes: 44 additions & 0 deletions src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,50 @@ public ValueBindingExpression<TResult> CreateValueBinding<TResult>(string code,
}));
}

/// <summary> Compiles a new `{resource: ...code...}` binding which can be evaluated server-side. The result is cached. <see cref="ResourceBindingExpression.ResourceBindingExpression(BindingCompilationService, IEnumerable{object})" /> </summary>
public ResourceBindingExpression CreateResourceBinding(string code, DataContextStack dataContext, BindingParserOptions? parserOptions = null)
{
return CreateCachedBinding("ResourceBinding:" + code, new object?[] { dataContext, parserOptions }, () =>
new ResourceBindingExpression(compilationService, new object?[] {
dataContext,
new OriginalStringBindingProperty(code),
parserOptions
}));
}

/// <summary> Compiles a new `{resource: ...code...}` binding which can be evaluated server-side. The result is implicitly converted to <typeparamref name="TResult" />. The result is cached. </summary>
public ResourceBindingExpression<TResult> CreateResourceBinding<TResult>(string code, DataContextStack dataContext, BindingParserOptions? parserOptions = null)
{
return CreateCachedBinding($"ResourceBinding<{typeof(TResult).ToCode()}>:{code}", new object?[] { dataContext, parserOptions }, () =>
new ResourceBindingExpression<TResult>(compilationService, new object?[] {
dataContext,
new OriginalStringBindingProperty(code),
parserOptions
}));
}

/// <summary> Compiles a new `{command: ...code...}` binding which can be evaluated server-side and also client-side. The result is cached. Note that command bindings might be easier to create using the <see cref="CommandBindingExpression.CommandBindingExpression(BindingCompilationService, Func{object[], System.Threading.Tasks.Task}, string)" /> constructor. </summary>
public CommandBindingExpression CreateCommand(string code, DataContextStack dataContext, BindingParserOptions? parserOptions = null)
{
return CreateCachedBinding($"Command:{code}", new object?[] { dataContext, parserOptions }, () =>
new CommandBindingExpression(compilationService, new object?[] {
dataContext,
new OriginalStringBindingProperty(code),
parserOptions
}));
}

/// <summary> Compiles a new `{command: ...code...}` binding which can be evaluated server-side and also client-side. The result is implicitly converted to <typeparamref name="TResult" />. The result is cached. Note that command bindings might be easier to create using the <see cref="CommandBindingExpression.CommandBindingExpression(BindingCompilationService, Func{object[], System.Threading.Tasks.Task}, string)" /> constructor. </summary>
public CommandBindingExpression<TResult> CreateCommand<TResult>(string code, DataContextStack dataContext, BindingParserOptions? parserOptions = null)
{
return CreateCachedBinding($"Command<{typeof(TResult).ToCode()}>:{code}", new object?[] { dataContext, parserOptions }, () =>
new CommandBindingExpression<TResult>(compilationService, new object?[] {
dataContext,
new OriginalStringBindingProperty(code),
parserOptions
}));
}

/// <summary> Compiles a new `{staticCommand: ...code...}` binding which can be evaluated server-side and also client-side. The result is cached. </summary>
public StaticCommandBindingExpression CreateStaticCommand(string code, DataContextStack dataContext, BindingParserOptions? parserOptions = null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,19 +192,17 @@ public static (LambdaExpression getter, LambdaExpression setter) CreatePropertyA
if (canUseDirectAccess && unwrappedType.IsValueType)
{
getValue = Call(typeof(Helpers), nameof(Helpers.GetStructValueDirect), new Type[] { unwrappedType }, currentControlParameter, Constant(property));
if (type.IsNullable())
if (!type.IsNullable())
getValue = Expression.Property(getValue, "Value");
}
else
{
getValue = Call(currentControlParameter, getValueMethod, Constant(property), Constant(property.IsValueInherited));
getValue = Convert(getValue, type);
}
return (
Expression.Lambda(
Expression.Convert(
Expression.Call(currentControlParameter, getValueMethod, Expression.Constant(property), Expression.Constant(false)),
type
),
getValue,
currentControlParameter
),
Expression.Lambda(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ internal static class Helpers
public static ValueOrBinding<T>? GetOptionalValueOrBinding<T>(DotvvmBindableObject c, DotvvmProperty p)
{
if (c.properties.TryGet(p, out var x))
return ValueOrBinding<T>.FromBoxedValue(x);
{
// we want to return ValueOrBinding(null) -- "property set to null"
// but if that isn't possible, it's better to return null ("property missing") than crash
if (x is null && default(T) != null)
return null;
else
return ValueOrBinding<T>.FromBoxedValue(x);
}
else return null;
}
public static ValueOrBinding<T> GetValueOrBinding<T>(DotvvmBindableObject c, DotvvmProperty p)
Expand All @@ -27,7 +34,15 @@ public static ValueOrBinding<T> GetValueOrBinding<T>(DotvvmBindableObject c, Dot
public static ValueOrBinding<T>? GetOptionalValueOrBindingSlow<T>(DotvvmBindableObject c, DotvvmProperty p)
{
if (c.IsPropertySet(p))
return ValueOrBinding<T>.FromBoxedValue(c.GetValue(p));
{
var x = c.GetValue(p);
// we want to return ValueOrBinding(null) -- "property set to null"
// but if that isn't possible, it's better to return null ("property missing") than crash
if (x is null && default(T) != null)
return null;
else
return ValueOrBinding<T>.FromBoxedValue(x);
}
else return null;
}
public static ValueOrBinding<T> GetValueOrBindingSlow<T>(DotvvmBindableObject c, DotvvmProperty p)
Expand Down Expand Up @@ -65,7 +80,7 @@ public static void SetOptionalValueOrBindingSlow<T>(DotvvmBindableObject c, Dotv
public static void SetValueOrBindingSlow<T>(DotvvmBindableObject c, DotvvmProperty p, ValueOrBinding<T> val)
{
var boxedVal = val.UnwrapToObject();
if (Object.Equals(boxedVal, p.DefaultValue) && c.IsPropertySet(p))
if (Object.Equals(boxedVal, p.DefaultValue) && !c.IsPropertySet(p))
{
// setting to default value and the property is not set -> do nothing
}
Expand All @@ -86,6 +101,8 @@ public static void SetValueOrBindingSlow<T>(DotvvmBindableObject c, DotvvmProper
public static T? GetStructValueDirect<T>(DotvvmBindableObject c, DotvvmProperty p)
where T: struct
{
// T being a struct allows us to invert the rather expensive `is IBinding` typecheck in EvalPropertyValue
// to a simpler is T check, as T is a single type checkable with a simple comparison
if (c.properties.TryGet(p, out var x))
{
if (x is null)
Expand Down
6 changes: 5 additions & 1 deletion src/Framework/Framework/Binding/DotvvmProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ internal void AddUsedInCapability(DotvvmCapabilityProperty? p)
if (p is object)
lock(this)
{
UsedInCapabilities = UsedInCapabilities.Add(p);
if (UsedInCapabilities.Contains(p)) return;

var newArray = UsedInCapabilities.Add(p);
Thread.MemoryBarrier(); // make sure the array is complete before we let other threads use it lock-free
UsedInCapabilities = newArray;
}
}

Expand Down
Loading

0 comments on commit 592e1dc

Please sign in to comment.