diff --git a/src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs b/src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs index d4fe0a140a..e99c23e5e1 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs @@ -132,8 +132,8 @@ internal sealed partial class TexlBinding /// /// The maximum number of selects in a table that will be included in data call. /// - public const int MaxSelectsToInclude = 100; - + public const int MaxSelectsToInclude = 100; + /// /// Default name used to access a Lambda scope. /// @@ -350,7 +350,7 @@ private TexlBinding( entityName: EntityName, propertyName: Property?.InvariantName ?? string.Empty, allowsSideEffects: bindingConfig.AllowsSideEffects, - numberIsFloat: bindingConfig.NumberIsFloat, + numberIsFloat: bindingConfig.NumberIsFloat, analysisMode: bindingConfig.AnalysisMode); } @@ -431,14 +431,14 @@ public static TexlBinding Run( var txb = new TexlBinding(glue, scopeResolver, queryOptionsMap, node, resolver, bindingConfig, ruleScope, updateDisplayNames, forceUpdateDisplayNames, rule: rule, features: features, delegationHintProvider: delegationHintProvider); var vis = new Visitor(txb, resolver, ruleScope, bindingConfig.UseThisRecordForRuleScope, features); - vis.Run(); - - // If the expression is dataflow only Ie non-side effecting, void doesn't have any practical use and should be converted to errors. - if (!bindingConfig.AllowsSideEffects) - { - var v2e = new VoidToErrorTexlVisitor(txb); - v2e.Run(); - } + vis.Run(); + + // If the expression is dataflow only Ie non-side effecting, void doesn't have any practical use and should be converted to errors. + if (!bindingConfig.AllowsSideEffects) + { + var v2e = new VoidToErrorTexlVisitor(txb); + v2e.Run(); + } // Determine if a rename has occured at the top level if (txb.Top is AsNode asNode) @@ -767,7 +767,7 @@ private bool TryGetEntityInfo(CallNode node, out IExpandInfo info) // It is possible for function to be null here if it referred to // a service function from a service we are in the process of - // deregistering. + // deregistering. // GetInfo on a callNode may return null hence need null conditional operator and it short circuits if null. return GetInfo(callNode)?.VerifyValue().Function?.TryGetEntityInfo(node, this, out info) ?? false; } @@ -2581,43 +2581,43 @@ public override void Visit(BlankNode node) _txb.SetSelfContainedConstant(node, true); _txb.SetType(node, DType.ObjNull); } - - // Binding TypeLiteralNode from anywhere other than valid type context should be an error. - // This ensures that binding of unintended use of TypeLiteralNode eg: "If(Type(Boolean), 1, 2)" will result in an error. + + // Binding TypeLiteralNode from anywhere other than valid type context should be an error. + // This ensures that binding of unintended use of TypeLiteralNode eg: "If(Type(Boolean), 1, 2)" will result in an error. // VisitType method is used to resolve the type of TypeLiteralNode from valid context. public override void Visit(TypeLiteralNode node) { AssertValid(); - Contracts.AssertValue(node); - - _txb.SetType(node, DType.Error); + Contracts.AssertValue(node); + + _txb.SetType(node, DType.Error); _txb.ErrorContainer.Error(node, TexlStrings.ErrTypeFunction_UnsupportedUsage); - } - - // Method to bind TypeLiteralNode from valid context where a type is expected. - // Binding TypeLiteralNode in an expression where a type is not expected invokes the Visit method - // from normal visitor pattern and results in error. + } + + // Method to bind TypeLiteralNode from valid context where a type is expected. + // Binding TypeLiteralNode in an expression where a type is not expected invokes the Visit method + // from normal visitor pattern and results in error. private void VisitType(TypeLiteralNode node) { AssertValid(); - Contracts.AssertValue(node); - - if (_nameResolver == null) - { - _txb.SetType(node, DType.Unknown); - return; - } - - var type = DTypeVisitor.Run(node.TypeRoot, _nameResolver); - - if (type.IsValid) - { - _txb.SetType(node, type); - } - else - { - _txb.SetType(node, DType.Error); - _txb.ErrorContainer.Error(node, TexlStrings.ErrTypeFunction_InvalidTypeExpression, node.ToString()); + Contracts.AssertValue(node); + + if (_nameResolver == null) + { + _txb.SetType(node, DType.Unknown); + return; + } + + var type = DTypeVisitor.Run(node.TypeRoot, _nameResolver); + + if (type.IsValid) + { + _txb.SetType(node, type); + } + else + { + _txb.SetType(node, DType.Error); + _txb.ErrorContainer.Error(node, TexlStrings.ErrTypeFunction_InvalidTypeExpression, node.ToString()); } } @@ -2834,20 +2834,20 @@ public override void Visit(FirstNameNode node) return; } - DType nodeType = null; - - if (scope.ScopeIdentifiers == null || scope.ScopeIdentifiers.Length == 1) - { - nodeType = scope.Type; - } - else - { - // If scope.ScopeIdentifier.Length > 1, it meant the function creates more than 1 scope and the scope types are contained within a record. - // Example: Join(t1, t2, LeftRecord.a = RightRecord.a, ...) - // The expression above will create LeftRecord and RightRecord scopes. The scope type will be ![LeftRecord:![...],RightRecord:![...]] - // Example: Join(t1 As X1, t2 As X2, X1.a = X2.a, ...) - // The expression above will create LeftRecord and RightRecord scopes. The scope type will be ![X1:![...],X2:![...]] - nodeType = scope.Type.GetType(nodeName); + DType nodeType = null; + + if (scope.ScopeIdentifiers == null || scope.ScopeIdentifiers.Length == 1) + { + nodeType = scope.Type; + } + else + { + // If scope.ScopeIdentifier.Length > 1, it meant the function creates more than 1 scope and the scope types are contained within a record. + // Example: Join(t1, t2, LeftRecord.a = RightRecord.a, ...) + // The expression above will create LeftRecord and RightRecord scopes. The scope type will be ![LeftRecord:![...],RightRecord:![...]] + // Example: Join(t1 As X1, t2 As X2, X1.a = X2.a, ...) + // The expression above will create LeftRecord and RightRecord scopes. The scope type will be ![X1:![...],X2:![...]] + nodeType = scope.Type.GetType(nodeName); } if (!isWholeScope) @@ -2896,16 +2896,16 @@ public override void Visit(FirstNameNode node) _txb.SetType(node, DType.Error); _txb.SetInfo(node, FirstNameInfo.Create(node, default(NameLookupInfo))); return; - } - - // We have an allowlist of kinds permitted in simple expressions, all of which should be Sync. - // The IsAsync check is just to be sure we're not introducing async - // if things are added to the set of valid kinds in the future. - // As the main point of the "Simple Expression" constraint is to ensure certain expressions are sync - // but that's harder to communicate to low-code users. - if (_txb.BindingConfig.EnforceSimpleExpressionConstraint && (!lookupInfo.Kind.IsValidInSimpleExpression() || lookupInfo.IsAsync)) - { - _txb.ErrorContainer.Error(node, TexlStrings.ErrViolatedSimpleConstraintAccess, node.Ident.Name.Value); + } + + // We have an allowlist of kinds permitted in simple expressions, all of which should be Sync. + // The IsAsync check is just to be sure we're not introducing async + // if things are added to the set of valid kinds in the future. + // As the main point of the "Simple Expression" constraint is to ensure certain expressions are sync + // but that's harder to communicate to low-code users. + if (_txb.BindingConfig.EnforceSimpleExpressionConstraint && (!lookupInfo.Kind.IsValidInSimpleExpression() || lookupInfo.IsAsync)) + { + _txb.ErrorContainer.Error(node, TexlStrings.ErrViolatedSimpleConstraintAccess, node.Ident.Name.Value); } var isConstantNamedFormula = false; @@ -2916,29 +2916,29 @@ public override void Visit(FirstNameNode node) _txb.SetSetMutable(node, nameSymbol?.Props.CanSetMutate ?? false); if (lookupInfo.Data is IExternalNamedFormula formula) { - isConstantNamedFormula = formula.IsConstant; - - // If the definition of the named formula has a delegation warning, every use should also inherit this warning - if (formula.HasDelegationWarning) - { - _txb.ErrorContainer.EnsureError(DocumentErrorSeverity.Warning, node, TexlStrings.SuggestRemoteExecutionHint_NF, node.Ident.Name); + isConstantNamedFormula = formula.IsConstant; + + // If the definition of the named formula has a delegation warning, every use should also inherit this warning + if (formula.HasDelegationWarning) + { + _txb.ErrorContainer.EnsureError(DocumentErrorSeverity.Warning, node, TexlStrings.SuggestRemoteExecutionHint_NF, node.Ident.Name); } } } - else if (lookupInfo.Kind == BindKind.Data) - { - if (lookupInfo.Data is IExternalCdsDataSource or IExternalTabularDataSource) - { - _txb.SetMutable(node, true); - } - else if (lookupInfo.Data is IExternalDataSource ds) - { - _txb.SetMutable(node, ds.IsWritable); - } - } - else if (lookupInfo.Kind == BindKind.ScopeCollection) - { - _txb.SetMutable(node, true); + else if (lookupInfo.Kind == BindKind.Data) + { + if (lookupInfo.Data is IExternalCdsDataSource or IExternalTabularDataSource) + { + _txb.SetMutable(node, true); + } + else if (lookupInfo.Data is IExternalDataSource ds) + { + _txb.SetMutable(node, ds.IsWritable); + } + } + else if (lookupInfo.Kind == BindKind.ScopeCollection) + { + _txb.SetMutable(node, true); } else if (lookupInfo.Kind == BindKind.ScopeCollection) { @@ -3053,14 +3053,14 @@ public override void Visit(FirstNameNode node) { _txb.ErrorContainer.EnsureError(node, TexlStrings.ErrInvalidControlReference); } - } - - if (_txb.BindingConfig.MarkAsAsyncOnLazilyLoadedControlRef && - lookupType.IsControl && - lookupInfo.Data is IExternalControl control && - !control.IsAppGlobalControl) - { - _txb.FlagPathAsAsync(_txb.Top); + } + + if (_txb.BindingConfig.MarkAsAsyncOnLazilyLoadedControlRef && + lookupType.IsControl && + lookupInfo.Data is IExternalControl control && + !control.IsAppGlobalControl) + { + _txb.FlagPathAsAsync(_txb.Top); } // Update _usesGlobals, _usesResources, etc. @@ -3594,7 +3594,7 @@ public override void PostVisit(DottedNameNode node) // If the reference is to Control.Property and the rule for that Property is a constant, // we need to mark the node as constant, and save the control info so we may look up the // rule later. - if (controlInfo?.GetRule(property.InvariantName) is IExternalRule rule && + if (controlInfo?.GetRule(property.InvariantName) is IExternalRule rule && rule.IsInvariantExpression) { value = controlInfo; @@ -3853,16 +3853,16 @@ protected DType GetExpandedEntityType(DType expandEntityType, string relatedEnti // Update the datasource and relatedEntity path. type.ExpandInfo.UpdateEntityInfo(expandEntityInfo.ParentDataSource, relatedEntityPath); entityTypes.Add(expandEntityInfo.ExpandPath, type); - } - else if (!type.ExpandInfo.ExpandPath.IsReachedFromPath(relatedEntityPath)) - { - // Expands reached via a different path should have a different relatedentitypath. - // If we found an expand in the cache but it's not accessed via the same relationship - // we need to create a different expand info but with the same type. - // DType.Clone doesn't clone expand info, so we force that with CopyExpandInfo, - // because that sadly mutates expand info on what should otherwise be an immutable dtype. + } + else if (!type.ExpandInfo.ExpandPath.IsReachedFromPath(relatedEntityPath)) + { + // Expands reached via a different path should have a different relatedentitypath. + // If we found an expand in the cache but it's not accessed via the same relationship + // we need to create a different expand info but with the same type. + // DType.Clone doesn't clone expand info, so we force that with CopyExpandInfo, + // because that sadly mutates expand info on what should otherwise be an immutable dtype. type = DType.CopyExpandInfo(type.Clone(), type); - type.ExpandInfo.UpdateEntityInfo(expandEntityInfo.ParentDataSource, relatedEntityPath); + type.ExpandInfo.UpdateEntityInfo(expandEntityInfo.ParentDataSource, relatedEntityPath); } return type; @@ -4140,13 +4140,13 @@ private void SetCallNodePurity(CallNode node, CallInfo info) var infoTexlFunction = info.Function; if (_txb._glue.IsComponentScopedPropertyFunction(infoTexlFunction)) - { - // Behavior only component properties should be treated as stateful. - hasSideEffects |= infoTexlFunction.IsBehaviorOnly; - - // At the moment, we're going to treat all invocations of component scoped property functions as stateful. - // This ensures that we don't lift these function invocations in loops, and that they are re-evaluated every time they are called, - // which is always correct, although less efficient in some cases. + { + // Behavior only component properties should be treated as stateful. + hasSideEffects |= infoTexlFunction.IsBehaviorOnly; + + // At the moment, we're going to treat all invocations of component scoped property functions as stateful. + // This ensures that we don't lift these function invocations in loops, and that they are re-evaluated every time they are called, + // which is always correct, although less efficient in some cases. isStateFul |= true; } else @@ -4312,40 +4312,40 @@ private void UntypedObjectScopeError(CallNode node, TexlFunction maybeFunc, Texl _txb.SetInfo(node, new CallInfo(maybeFunc, node, null, default, false, _currentScope.Nest)); _txb.SetType(node, maybeFunc.ReturnType); - } - - // Checks if the call node best matches function overloads with UntypedObject/JSON, - // or the typed overload for the Copilot function - private bool MatchOverloadWithUntypedOrJSONConversionOrCopilotFunctions(CallNode node, TexlFunction maybeFunc) - { - Contracts.AssertValue(node); - Contracts.AssertValue(maybeFunc); - Contracts.Assert(maybeFunc.HasTypeArgs); - - if (maybeFunc.Name == AsTypeFunction.AsTypeInvariantFunctionName && - _txb.GetType(node.Args.Children[0]) == DType.UntypedObject) - { - return true; - } - - if (maybeFunc.Name == IsTypeFunction_UO.IsTypeInvariantFunctionName && - _txb.GetType(node.Args.Children[0]) == DType.UntypedObject) - { - return true; - } - - if (maybeFunc.Name == ParseJSONFunction.ParseJSONInvariantFunctionName && - node.Args.Count > 1) - { - return true; - } - - if (maybeFunc is CopilotFunction && node.Args.Count > 2) - { - return true; - } - - return false; + } + + // Checks if the call node best matches function overloads with UntypedObject/JSON, + // or the typed overload for the Copilot function + private bool MatchOverloadWithUntypedOrJSONConversionOrCopilotFunctions(CallNode node, TexlFunction maybeFunc) + { + Contracts.AssertValue(node); + Contracts.AssertValue(maybeFunc); + Contracts.Assert(maybeFunc.HasTypeArgs); + + if (maybeFunc.Name == AsTypeFunction.AsTypeInvariantFunctionName && + _txb.GetType(node.Args.Children[0]) == DType.UntypedObject) + { + return true; + } + + if (maybeFunc.Name == IsTypeFunction_UO.IsTypeInvariantFunctionName && + _txb.GetType(node.Args.Children[0]) == DType.UntypedObject) + { + return true; + } + + if (maybeFunc.Name == ParseJSONFunction.ParseJSONInvariantFunctionName && + node.Args.Count > 1) + { + return true; + } + + if (maybeFunc is CopilotFunction && node.Args.Count > 2) + { + return true; + } + + return false; } public override bool PreVisit(CallNode node) @@ -4366,7 +4366,7 @@ public override bool PreVisit(CallNode node) if (BuiltinFunctionsCore.OtherKnownFunctions.Contains(node.Head.Name.Value, StringComparer.OrdinalIgnoreCase)) { _txb.ErrorContainer.Error(node, TexlStrings.ErrUnimplementedFunction, node.Head.Name.Value); - } + } else if (BuiltinFunctionsCore.TypeHelperFunctions.Contains(node.Head.Name.Value, StringComparer.OrdinalIgnoreCase)) { _txb.ErrorContainer.Error(node, TexlStrings.ErrKnownTypeHelperFunction, node.Head.Name.Value); @@ -4400,9 +4400,9 @@ public override bool PreVisit(CallNode node) // If there are no overloads with lambdas or identifiers, we can continue the visitation and // yield to the normal overload resolution. - var overloadsWithLambdasOrIdentifiers = overloads.Where(func => func.HasLambdas || func.HasColumnIdentifiers); - - var overloadsWithTypeArgs = overloads.Where(func => func.HasTypeArgs); + var overloadsWithLambdasOrIdentifiers = overloads.Where(func => func.HasLambdas || func.HasColumnIdentifiers); + + var overloadsWithTypeArgs = overloads.Where(func => func.HasTypeArgs); if (!overloadsWithLambdasOrIdentifiers.Any()) { @@ -4436,23 +4436,23 @@ public override bool PreVisit(CallNode node) if (overloadsWithTypeArgs.Any() && node.Args.Count > 1) { - Contracts.Assert(overloadsWithTypeArgs.Count() == 1); - - // Evaluates the first argument; required for the call to - // MatchOverloadWithUntypedOrJSONConversionFunctions - var firstArg = node.Args.Children[0]; - firstArg.Accept(this); - - var functionWithTypeArg = overloadsWithTypeArgs.First(); - - // Either one of the untyped JSON conversion functions, or the overload + Contracts.Assert(overloadsWithTypeArgs.Count() == 1); + + // Evaluates the first argument; required for the call to + // MatchOverloadWithUntypedOrJSONConversionFunctions + var firstArg = node.Args.Children[0]; + firstArg.Accept(this); + + var functionWithTypeArg = overloadsWithTypeArgs.First(); + + // Either one of the untyped JSON conversion functions, or the overload // for the Copilot function that takes type args - if (MatchOverloadWithUntypedOrJSONConversionOrCopilotFunctions(node, functionWithTypeArg)) + if (MatchOverloadWithUntypedOrJSONConversionOrCopilotFunctions(node, functionWithTypeArg)) { - PreVisitTypeArgAndProccesCallNode(node, functionWithTypeArg); - FinalizeCall(node); + PreVisitTypeArgAndProccesCallNode(node, functionWithTypeArg); + FinalizeCall(node); return false; - } + } startArg++; } @@ -4517,16 +4517,16 @@ public override bool PreVisit(CallNode node) scopeNew = new Scope(node, _currentScope, scopeInfo.ScopeType, skipForInlineRecords: maybeFunc.SkipScopeForInlineRecords); } else if (carg > 0) - { - // Gets the lesser number between the CallNode chidl args and func.ScopeArgs. - // There reason why is that the intellisense can visit this code and provide a number of args less than the func.ScopeArgs. - // Example: Join(| - var argsCount = Math.Min(node.Args.Children.Count, maybeFunc.ScopeArgs); - var types = new DType[argsCount]; - - for (int i = 0; i < argsCount; i++) - { - node.Args.Children[i].Accept(this); + { + // Gets the lesser number between the CallNode chidl args and func.ScopeArgs. + // There reason why is that the intellisense can visit this code and provide a number of args less than the func.ScopeArgs. + // Example: Join(| + var argsCount = Math.Min(node.Args.Children.Count, maybeFunc.ScopeArgs); + var types = new DType[argsCount]; + + for (int i = 0; i < argsCount; i++) + { + node.Args.Children[i].Accept(this); } // At this point we know the type of the first argument, so we can check for untyped objects @@ -4536,7 +4536,7 @@ public override bool PreVisit(CallNode node) scopeInfo = maybeFunc.ScopeInfo; } - // Determine the Scope Identifier using the func.ScopeArgs arg + // Determine the Scope Identifier using the func.ScopeArgs arg required = scopeInfo.GetScopeIdent(node.Args.Children.ToArray(), out scopeIdentifiers); if (scopeInfo.CheckInput(_txb.Features, node, node.Args.Children.ToArray(), out scope, GetScopeArgsTypes(node.Args.Children, argsCount))) @@ -4614,14 +4614,14 @@ public override bool PreVisit(CallNode node) { _currentScopeDsNodeId = dsNode.Id; } - - argTypes[0] = _txb.GetType(nodeInput); + + argTypes[0] = _txb.GetType(nodeInput); // Get the cursor type for this arg. Note we're not adding document errors at this point. DType typeScope; DName[] scopeIdent = default; var identRequired = false; - var fArgsValid = true; + var fArgsValid = true; if (scopeInfo.ScopeType != null) { @@ -4630,17 +4630,17 @@ public override bool PreVisit(CallNode node) // For functions with a Scope Type, there is no ScopeIdent needed } else - { - // Starting from 1 since 0 was visited above. - for (int i = 1; i < maybeFunc.ScopeArgs; i++) - { - _txb.AddVolatileVariables(node, _txb.GetVolatileVariables(args[i])); - args[i].Accept(this); - } - + { + // Starting from 1 since 0 was visited above. + for (int i = 1; i < maybeFunc.ScopeArgs; i++) + { + _txb.AddVolatileVariables(node, _txb.GetVolatileVariables(args[i])); + args[i].Accept(this); + } + fArgsValid = scopeInfo.CheckInput(_txb.Features, node, args, out typeScope, GetScopeArgsTypes(node.Args.Children, maybeFunc.ScopeArgs)); - // Determine the scope identifier using the first node for lambda params + // Determine the scope identifier using the first node for lambda params identRequired = scopeInfo.GetScopeIdent(args, out scopeIdent); } @@ -4713,15 +4713,15 @@ public override bool PreVisit(CallNode node) _currentScope = (isIdentifier || isLambdaArg) ? scopeNew : scopeNew.Parent; if (!isIdentifier || maybeFunc.GetIdentifierParamStatus(args[i], _features, i) == TexlFunction.ParamIdentifierStatus.PossiblyIdentifier) - { - if (_txb.GetTypeAllowInvalid(args[i]) != null && !_txb.GetTypeAllowInvalid(args[i]).IsValid) - { - args[i].Accept(this); - _txb.AddVolatileVariables(node, _txb.GetVolatileVariables(args[i])); - } - - argTypes[i] = _txb.GetType(args[i]); - + { + if (_txb.GetTypeAllowInvalid(args[i]) != null && !_txb.GetTypeAllowInvalid(args[i]).IsValid) + { + args[i].Accept(this); + _txb.AddVolatileVariables(node, _txb.GetVolatileVariables(args[i])); + } + + argTypes[i] = _txb.GetType(args[i]); + Contracts.Assert(argTypes[i].IsValid); } else @@ -4788,17 +4788,17 @@ public override bool PreVisit(CallNode node) // We fully processed the call, so don't visit children or call PostVisit. return false; - } - - /// - /// Get all DType used to compose the scope of the function (func.ScopeArgs). - /// - /// Call child nodes. - /// TexlFunction ScopeArgs property. - /// DType array. - private DType[] GetScopeArgsTypes(IReadOnlyList args, int scopeArgs) - { - return args.Take(scopeArgs).Select(node => _txb.GetType(node)).ToArray(); + } + + /// + /// Get all DType used to compose the scope of the function (func.ScopeArgs). + /// + /// Call child nodes. + /// TexlFunction ScopeArgs property. + /// DType array. + private DType[] GetScopeArgsTypes(IReadOnlyList args, int scopeArgs) + { + return args.Take(scopeArgs).Select(node => _txb.GetType(node)).ToArray(); } private void FinalizeCall(CallNode node) @@ -4853,14 +4853,14 @@ private void FinalizeCall(CallNode node) // Invalid datasources always result in error if (func.IsBehaviorOnly && !_txb.BindingConfig.AllowsSideEffects) - { - if (_txb.BindingConfig.UserDefinitionsMode) - { - _txb.ErrorContainer.EnsureError(node, TexlStrings.ErrBehaviorFunctionInDataUDF); - } - else - { - _txb.ErrorContainer.EnsureError(node, TexlStrings.ErrBehaviorPropertyExpected); + { + if (_txb.BindingConfig.UserDefinitionsMode) + { + _txb.ErrorContainer.EnsureError(node, TexlStrings.ErrBehaviorFunctionInDataUDF); + } + else + { + _txb.ErrorContainer.EnsureError(node, TexlStrings.ErrBehaviorPropertyExpected); } } @@ -4868,14 +4868,14 @@ private void FinalizeCall(CallNode node) else if (func.IsTestOnly && _txb.Property != null && !_txb.Property.IsTestCaseProperty) { _txb.ErrorContainer.EnsureError(node, TexlStrings.ErrTestPropertyExpected); - } - else if ((!func.IsAllowedInSimpleExpressions || _txb.IsAsync(node)) && _txb.BindingConfig.EnforceSimpleExpressionConstraint) - { - // Functions that are not allowed in simple expressions cannot be used when the binding config restricts to simple expressions. - _txb.ErrorContainer.EnsureError(node, TexlStrings.ErrViolatedSimpleConstraintFunction, func.Name); - } - - // Auto-refreshable functions cannot be used in behavior rules. + } + else if ((!func.IsAllowedInSimpleExpressions || _txb.IsAsync(node)) && _txb.BindingConfig.EnforceSimpleExpressionConstraint) + { + // Functions that are not allowed in simple expressions cannot be used when the binding config restricts to simple expressions. + _txb.ErrorContainer.EnsureError(node, TexlStrings.ErrViolatedSimpleConstraintFunction, func.Name); + } + + // Auto-refreshable functions cannot be used in behavior rules. else if (func.IsAutoRefreshable && _txb.BindingConfig.AllowsSideEffects) { _txb.ErrorContainer.EnsureError(node, TexlStrings.ErrAutoRefreshNotAllowed); @@ -4903,12 +4903,12 @@ private void FinalizeCall(CallNode node) { _txb.ErrorContainer.EnsureError(node, errorKey, badAncestor.Head.Name); } - } - - // If the definition of the user-defined function has a delegation warning, every usage should also inherit this warning - if (func is UserDefinedFunction udf && udf.HasDelegationWarning) - { - _txb.ErrorContainer.EnsureError(DocumentErrorSeverity.Warning, node, TexlStrings.SuggestRemoteExecutionHint_UDF, udf.Name); + } + + // If the definition of the user-defined function has a delegation warning, every usage should also inherit this warning + if (func is UserDefinedFunction udf && udf.HasDelegationWarning) + { + _txb.ErrorContainer.EnsureError(DocumentErrorSeverity.Warning, node, TexlStrings.SuggestRemoteExecutionHint_UDF, udf.Name); } _txb.CheckAndMarkAsDelegatable(node); @@ -4973,9 +4973,20 @@ private bool IsIncorrectlySideEffectful(CallNode node, out ErrorResourceKey erro var ancestorScopeInfo = ancestorCall.Function?.ScopeInfo; // Check for bad scope modification - if (ancestorFunc != null && ancestorScopeInfo != null && ds != null && ancestorScopeInfo.IteratesOverScope) + if (ancestorFunc != null && ancestorScopeInfo != null && ancestorScopeInfo.IteratesOverScope) { - if (ancestorFunc.TryGetDataSource(ancestorScope.Call, _txb, out var ancestorDs) && ancestorDs == ds) + if (ds != null && ancestorFunc.TryGetDataSource(ancestorScope.Call, _txb, out var ancestorDs) && ancestorDs == ds) + { + errorKey = TexlStrings.ErrScopeModificationLambda; + badAncestor = ancestorScope.Call; + return true; + } + + // Also check for global table variables (not external data sources) + if (_txb.Features.EnhancedIterationChecks && + TryGetFirstArgGlobalTableVariable(node, out var modifiedVar) && + TryGetFirstArgGlobalTableVariable(ancestorScope.Call, out var iteratedVar) && + modifiedVar == iteratedVar) { errorKey = TexlStrings.ErrScopeModificationLambda; badAncestor = ancestorScope.Call; @@ -5002,6 +5013,41 @@ private bool IsIncorrectlySideEffectful(CallNode node, out ErrorResourceKey erro return false; } + // Returns true if the first argument of callNode is a global table variable, and outputs its name. + // Used for self modifying variable detection for ForAll and other iterator functions. + // + // This function will dive deep into the first argument of callNodes. So, it will extract the first argument of: + // + // top level: ForAll( DS, Set( DS, [...] ) ), lambda: Set( DS, [ ...] ) - match found, self modifying + // Self modifying and error produced. + // + // top level: ForAll( DS, Set( First(DS).Value, ... ) ), lambda: Set( First(DS).Value, ... ) - match NOT found due to lambda (two deep) + // We consider this OK and not self modifying as we are modifying a record of DS, not the structure of DS itself which would mess up the iteration. + // + // top level: ForAll( Filter(DS, Value > 0), Set( DS, [...] ) ), lambda: Set( DS, [...] ) - match NOT found due to top level (two deep) + // We consider this OK and not self modifying because DS is not delegable, so a copy is made for Filter to iterate over. + private bool TryGetFirstArgGlobalTableVariable(CallNode callNode, out DName variableName) + { + variableName = default; + + if (callNode.Args.Count < 1) + { + return false; + } + + if (callNode.Args.Children[0] is FirstNameNode firstName) + { + var info = _txb.GetInfo(firstName); + if (info?.Kind == BindKind.PowerFxResolvedObject && _txb.GetType(firstName).IsTable) + { + variableName = firstName.Ident.Name; + return true; + } + } + + return false; + } + public override void PostVisit(CallNode node) { Contracts.Assert(false, "Should never get here"); @@ -5113,9 +5159,9 @@ private void PreVisitMetadataArg(CallNode node, TexlFunction func) } else { - if (_txb.GetTypeAllowInvalid(args[i]) != null && !_txb.GetTypeAllowInvalid(args[i]).IsValid) - { - args[i].Accept(this); + if (_txb.GetTypeAllowInvalid(args[i]) != null && !_txb.GetTypeAllowInvalid(args[i]).IsValid) + { + args[i].Accept(this); } } @@ -5166,78 +5212,78 @@ private void PreVisitMetadataArg(CallNode node, TexlFunction func) } _txb.SetType(node, returnType); - } - - // Method to previsit type arg of callnode if it is determined as untyped/json conversion function - private void PreVisitTypeArgAndProccesCallNode(CallNode node, TexlFunction func) - { + } + + // Method to previsit type arg of callnode if it is determined as untyped/json conversion function + private void PreVisitTypeArgAndProccesCallNode(CallNode node, TexlFunction func) + { AssertValid(); Contracts.AssertValue(node); - Contracts.AssertValue(func); - Contracts.Assert(func.HasTypeArgs); - Contracts.Assert(node.Args.Count != func.MaxArity || func.ArgIsType(node.Args.Count - 1)); // Type argument must be the last one for valid invocations - - var args = node.Args.Children.ToArray(); - var argCount = args.Count(); - - Contracts.AssertValue(_txb.GetType(args[0])); - + Contracts.AssertValue(func); + Contracts.Assert(func.HasTypeArgs); + Contracts.Assert(node.Args.Count != func.MaxArity || func.ArgIsType(node.Args.Count - 1)); // Type argument must be the last one for valid invocations + + var args = node.Args.Children.ToArray(); + var argCount = args.Count(); + + Contracts.AssertValue(_txb.GetType(args[0])); + if (argCount < func.MinArity || argCount > func.MaxArity) { ArityError(func.MinArity, func.MaxArity, node, argCount, _txb.ErrorContainer); _txb.SetInfo(node, new CallInfo(func, node)); _txb.SetType(node, DType.Error); return; - } - + } + if (!_features.IsUserDefinedTypesEnabled) { _txb.ErrorContainer.Error(node, TexlStrings.ErrUserDefinedTypesDisabled); _txb.SetInfo(node, new CallInfo(func, node)); _txb.SetType(node, DType.Error); return; - } - - Contracts.Assert(argCount > 1); - var typeArg = args[args.Length - 1]; - Contracts.AssertValue(typeArg); - - if (typeArg is FirstNameNode typeName) - { - if (_nameResolver.LookupType(typeName.Ident.Name, out var typeArgType)) - { - _txb.SetType(typeName, typeArgType._type); - _txb.SetInfo(typeName, FirstNameInfo.Create(typeName, new NameLookupInfo(BindKind.NamedType, typeArgType._type, DPath.Root, 0))); - } - else - { - _txb.ErrorContainer.Error(typeArg, TexlStrings.ErrInvalidName, typeName.Ident.Name.Value); - _txb.SetType(typeArg, DType.Error); - _txb.SetInfo(typeName, FirstNameInfo.Create(typeName, new NameLookupInfo(BindKind.NamedType, DType.Error, DPath.Root, 0))); - - _txb.ErrorContainer.Error(node, TexlStrings.ErrInvalidArgumentExpectedType, typeName.Ident.Name.Value); - _txb.SetInfo(node, new CallInfo(func, node)); - _txb.SetType(node, DType.Error); - } - } - else if (typeArg is TypeLiteralNode typeLiteral) - { - VisitType(typeLiteral); - } - else - { - _txb.ErrorContainer.Error(typeArg, TexlStrings.ErrInvalidArgumentExpectedType, typeArg); - _txb.SetType(typeArg, DType.Error); - } - - // Process other non-type arguments - for (var i = 1; i < args.Length - 1; i++) - { - args[i].Accept(this); - } - - PostVisit(node.Args); - + } + + Contracts.Assert(argCount > 1); + var typeArg = args[args.Length - 1]; + Contracts.AssertValue(typeArg); + + if (typeArg is FirstNameNode typeName) + { + if (_nameResolver.LookupType(typeName.Ident.Name, out var typeArgType)) + { + _txb.SetType(typeName, typeArgType._type); + _txb.SetInfo(typeName, FirstNameInfo.Create(typeName, new NameLookupInfo(BindKind.NamedType, typeArgType._type, DPath.Root, 0))); + } + else + { + _txb.ErrorContainer.Error(typeArg, TexlStrings.ErrInvalidName, typeName.Ident.Name.Value); + _txb.SetType(typeArg, DType.Error); + _txb.SetInfo(typeName, FirstNameInfo.Create(typeName, new NameLookupInfo(BindKind.NamedType, DType.Error, DPath.Root, 0))); + + _txb.ErrorContainer.Error(node, TexlStrings.ErrInvalidArgumentExpectedType, typeName.Ident.Name.Value); + _txb.SetInfo(node, new CallInfo(func, node)); + _txb.SetType(node, DType.Error); + } + } + else if (typeArg is TypeLiteralNode typeLiteral) + { + VisitType(typeLiteral); + } + else + { + _txb.ErrorContainer.Error(typeArg, TexlStrings.ErrInvalidArgumentExpectedType, typeArg); + _txb.SetType(typeArg, DType.Error); + } + + // Process other non-type arguments + for (var i = 1; i < args.Length - 1; i++) + { + args[i].Accept(this); + } + + PostVisit(node.Args); + var info = _txb.GetInfo(node); // If PreVisit resulted in errors for the node (and a non-null CallInfo), @@ -5251,27 +5297,27 @@ private void PreVisitTypeArgAndProccesCallNode(CallNode node, TexlFunction func) Contracts.AssertNull(info); - _txb.SetInfo(node, new CallInfo(func, node)); - - var returnType = func.ReturnType; + _txb.SetInfo(node, new CallInfo(func, node)); + + var returnType = func.ReturnType; var argTypes = args.Select(_txb.GetType).ToArray(); bool fArgsValid; var checkErrorContainer = new ErrorContainer(); // Typecheck the invocation and infer the return type. fArgsValid = func.HandleCheckInvocation(_txb, args, argTypes, checkErrorContainer, out returnType, out var _); - - if (checkErrorContainer?.HasErrors() == true) - { - _txb.ErrorContainer.MergeErrors(checkErrorContainer.GetErrors()); - } + + if (checkErrorContainer?.HasErrors() == true) + { + _txb.ErrorContainer.MergeErrors(checkErrorContainer.GetErrors()); + } if (!fArgsValid) { _txb.ErrorContainer.Error(DocumentErrorSeverity.Severe, node.Head.Token, TexlStrings.ErrInvalidArgs_Func, func.Name); } - _txb.SetType(node, returnType); + _txb.SetType(node, returnType); } private void PreVisitBottomUp(CallNode node, int argCountVisited, Scope scopeNew = null) @@ -5336,10 +5382,10 @@ private void PreVisitBottomUp(CallNode node, int argCountVisited, Scope scopeNew { _txb.AddVolatileVariables(args[i], volatileVariables); } - - if (_txb.GetTypeAllowInvalid(args[i]) != null && !_txb.GetTypeAllowInvalid(args[i]).IsValid) - { - args[i].Accept(this); + + if (_txb.GetTypeAllowInvalid(args[i]) != null && !_txb.GetTypeAllowInvalid(args[i]).IsValid) + { + args[i].Accept(this); } // In case weight was added during visitation @@ -5779,8 +5825,8 @@ public override void PostVisit(TableNode node) } } } - - // Tables that are created from sealed records become sealed + + // Tables that are created from sealed records become sealed // Among other things, this avoids someone writing First( Table( SealedRecord ) ) to remove the seal DType tableType = exprType.IsValid ? (_features.TableSyntaxDoesntWrapRecords && exprType.IsRecord diff --git a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/TexlTests.cs b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/TexlTests.cs index b8db9e2ce4..0987419b1a 100644 --- a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/TexlTests.cs +++ b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/TexlTests.cs @@ -4640,33 +4640,33 @@ public void TexlFunctionTypeSemanticsTable_PageableInputs(string script, string // self modifying, direct to DS delegable DS, enhanced , non-del DS, enhanced , variable DS, enhanced - [InlineData("ForAll(DS, Collect(DS, {Id:1})) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("ForAll(DS, Refresh(DS)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("ForAll(DS, Patch(DS,First(DS),{Id:2})) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("ForAll(DS, If(ThisRecord.Id>1, Patch(DS,First(DS),{Id:2}))) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("ForAll(DS, If(Id>1, Patch(DS,First(DS),{Id:2}))) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("AddColumns(DS, Num, Collect(DS, {Id:1}); 2) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("AddColumns(DS, Num, If(ThisRecord.Id>1,Collect(DS, {Id:1}); 2)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("AddColumns(DS, Num, If(Id>1,Collect(DS, {Id:1}); 2)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Concat(DS, Collect(DS, {Id:1}); Text(Id)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Distinct(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Sum(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Average(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Min(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Max(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("VarP(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("StdevP(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("ForAll(DS, ClearCollect(DS, {Id:1})) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " Unordered")] - [InlineData("ForAll(DS, Clear(DS)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " Unordered")] - [InlineData("AddColumns(DS, Num, ClearCollect(DS, {Id:1}); 2) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " Unordered")] - [InlineData("Concat(DS, Clear(DS); Text(Id)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " Unordered")] - [InlineData("Distinct(DS, ClearCollect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " Unordered")] - [InlineData("Sum(DS, Clear(DS); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " Unordered")] - [InlineData("Average(DS, ClearCollect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " Unordered")] - [InlineData("Min(DS, Clear(DS); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " Unordered")] - [InlineData("Max(DS, ClearCollect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " Unordered")] - [InlineData("VarP(DS, Clear(DS); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " Unordered")] - [InlineData("StdevP(DS, ClearCollect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " Unordered")] + [InlineData("ForAll(DS, Collect(DS, {Id:1})) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("ForAll(DS, Refresh(DS)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("ForAll(DS, Patch(DS,First(DS),{Id:2})) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("ForAll(DS, If(ThisRecord.Id>1, Patch(DS,First(DS),{Id:2}))) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("ForAll(DS, If(Id>1, Patch(DS,First(DS),{Id:2}))) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("AddColumns(DS, Num, Collect(DS, {Id:1}); 2) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("AddColumns(DS, Num, If(ThisRecord.Id>1,Collect(DS, {Id:1}); 2)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("AddColumns(DS, Num, If(Id>1,Collect(DS, {Id:1}); 2)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Concat(DS, Collect(DS, {Id:1}); Text(Id)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Distinct(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Sum(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Average(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Min(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Max(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("VarP(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("StdevP(DS, Collect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("ForAll(DS, ClearCollect(DS, {Id:1})) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " SelfMod")] + [InlineData("ForAll(DS, Clear(DS)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " SelfMod")] + [InlineData("AddColumns(DS, Num, ClearCollect(DS, {Id:1}); 2) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " SelfMod")] + [InlineData("Concat(DS, Clear(DS); Text(Id)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " SelfMod")] + [InlineData("Distinct(DS, ClearCollect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " SelfMod")] + [InlineData("Sum(DS, Clear(DS); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " SelfMod")] + [InlineData("Average(DS, ClearCollect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " SelfMod")] + [InlineData("Min(DS, Clear(DS); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " SelfMod")] + [InlineData("Max(DS, ClearCollect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " SelfMod")] + [InlineData("VarP(DS, Clear(DS); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " SelfMod")] + [InlineData("StdevP(DS, ClearCollect(DS, {Id:1}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Unordered", " SelfMod")] // self modifying, with Filter delegable DS, enhanced , non-del DS, enhanced , variable DS, enhanced diff --git a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/FileExpressionEvaluationTests.cs b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/FileExpressionEvaluationTests.cs index 22343098fe..2313749d25 100644 --- a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/FileExpressionEvaluationTests.cs +++ b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/FileExpressionEvaluationTests.cs @@ -240,8 +240,13 @@ public void RunOneMatchCompare() [Theory] [ReplFileSimpleList("MutationScripts")] public void RunMutationTests_V1(string file) - { - RunMutationTestFile(file, Features.PowerFxV1, "PowerFxV1"); + { + var features = new Features(Features.PowerFxV1) + { + EnhancedIterationChecks = true, + }; + + RunMutationTestFile(file, features, "PowerFxV1"); } [Theory] @@ -252,7 +257,8 @@ public void RunMutationTests_Canvas(string file) { TableSyntaxDoesntWrapRecords = true, ConsistentOneColumnTableResult = true, - IsUserDefinedTypesEnabled = true, + IsUserDefinedTypesEnabled = true, + EnhancedIterationChecks = true, }; // disable:PowerFxV1CompatibilityRules will force the tests specifically for those behaviors to be excluded from this run. diff --git a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationFunctionsTests.cs b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationFunctionsTests.cs index d263b33535..0dcb897cbb 100644 --- a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationFunctionsTests.cs +++ b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationFunctionsTests.cs @@ -636,22 +636,22 @@ public void MutationSuggestionTests(string expression, bool allowSideEffects, pa // Set self modifying, direct DS delegable DS, enhanced , non-del DS, enhanced , variable DS, enhanced - [InlineData("ForAll(DS, Set(DS, [{Id:1,Name:\"a\"}])) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] + [InlineData("ForAll(DS, Set(DS, [{Id:1,Name:\"a\"}])) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] [InlineData("ForAll(DS, Set(First(DS).Id, 3)) ", " Immutable", " Immutable", " Immutable", " Immutable", " Ok", " Ok")] - [InlineData("ForAll(DS, If(ThisRecord.Id>1, Set(DS, [{Id:1,Name:\"a\"}]))) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("ForAll(DS, If(Id>1, Set(DS, [{Id:1,Name:\"a\"}]))) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("AddColumns(DS, Num, Set(DS, [{Id:1,Name:\"a\"}]); 2) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] + [InlineData("ForAll(DS, If(ThisRecord.Id>1, Set(DS, [{Id:1,Name:\"a\"}]))) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("ForAll(DS, If(Id>1, Set(DS, [{Id:1,Name:\"a\"}]))) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("AddColumns(DS, Num, Set(DS, [{Id:1,Name:\"a\"}]); 2) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] [InlineData("AddColumns(DS, Num, Set(First(DS).Id, 3); 2) ", " Immutable", " Immutable", " Immutable", " Immutable", " Ok", " Ok")] - [InlineData("AddColumns(DS, Num, If(ThisRecord.Id>1,Set(DS, [{Id:1,Name:\"a\"}]); 2)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("AddColumns(DS, Num, If(Id>1,Set(DS, [{Id:1,Name:\"a\"}]); 2)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Concat(DS, Set(DS, [{Id:1,Name:\"a\"}]); Text(Id)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Distinct(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Sum(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Average(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Min(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Max(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("VarP(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("StdevP(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] + [InlineData("AddColumns(DS, Num, If(ThisRecord.Id>1,Set(DS, [{Id:1,Name:\"a\"}]); 2)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("AddColumns(DS, Num, If(Id>1,Set(DS, [{Id:1,Name:\"a\"}]); 2)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Concat(DS, Set(DS, [{Id:1,Name:\"a\"}]); Text(Id)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Distinct(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Sum(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Average(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Min(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Max(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("VarP(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("StdevP(DS, Set(DS, [{Id:1,Name:\"a\"}]); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] [InlineData("Concat(DS, Set(First(DS).Id, 3); Text(Id)) ", " Immutable", " Immutable", " Immutable", " Immutable", " Ok", " Ok")] [InlineData("Distinct(DS, Set(First(DS).Id, 3); Id) ", " Immutable", " Immutable", " Immutable", " Immutable", " Ok", " Ok")] [InlineData("Sum(DS, Set(First(DS).Id, 3); Id) ", " Immutable", " Immutable", " Immutable", " Immutable", " Ok", " Ok")] @@ -791,16 +791,16 @@ public void MutationSuggestionTests(string expression, bool allowSideEffects, pa // Remove self modifying, direct DS delegable DS, enhanced , non-del DS, enhanced , variable DS, enhanced - [InlineData("ForAll(DS, Remove(DS, {})) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("AddColumns(DS, Num, Remove(DS, {}); 2) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Concat(DS, Remove(DS, {}); Text(Id)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Distinct(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Sum(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Average(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Min(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("Max(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("VarP(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] - [InlineData("StdevP(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " Ok")] + [InlineData("ForAll(DS, Remove(DS, {})) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("AddColumns(DS, Num, Remove(DS, {}); 2) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Concat(DS, Remove(DS, {}); Text(Id)) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Distinct(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Sum(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Average(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Min(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("Max(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("VarP(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] + [InlineData("StdevP(DS, Remove(DS, {}); Id) ", " SelfMod", " SelfMod", " SelfMod", " SelfMod", " Ok", " SelfMod")] // Remove self modifying, with Sort delegable DS, enhanced , non-del DS, enhanced , variable DS, enhanced diff --git a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/ForAllMutate.txt b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/ForAllMutate.txt index e50f43b564..89fb147e4a 100644 --- a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/ForAllMutate.txt +++ b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/MutationScripts/ForAllMutate.txt @@ -36,7 +36,7 @@ Table({Value:2},{Value:4},{Value:6},{Value:8},{Value:10}) Blank() >> ForAll(t3,Patch(t3,ThisRecord,{Value:4})) -Blank() +Errors: Error 10-40: This function cannot operate on the same data source that is used in ForAll. >> 1;t3 Blank() @@ -55,7 +55,7 @@ Table() // Should do nothing as t4 is empty >> ForAll(t4,Patch(t4,ThisRecord,{Value:4})) -Table() +Errors: Error 10-40: This function cannot operate on the same data source that is used in ForAll. >> 2;t4 Table()