diff --git a/src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs b/src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs index 1a34766e6b..b5b2effcf1 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs @@ -123,7 +123,7 @@ internal sealed partial class TexlBinding // This is set when a First Name node or child First Name node contains itself in its variable weight // and can be read by the back end to determine whether it may generate code that lifts or caches an // expression - private readonly BitArray _isUnliftable; + private readonly BitArray _isUnliftable; public bool HasLocalScopeReferences { get; private set; } @@ -1124,6 +1124,21 @@ public void CheckAndMarkAsPageable(CallNode node, TexlFunction func) TransitionsFromAsyncToSync = true; } } + } + + public void CheckPredicateUsage(CallNode node, TexlFunction func) + { + Contracts.AssertValue(node); + Contracts.AssertValue(func); + + if (func.ScopeInfo != null && + func.ScopeInfo.CheckPredicateUsage && + !node.Args.ChildNodes.Any(child => child is ErrorNode) && + TryGetCall(node.Id, out var callInfo)) + { + //func.ScopeInfo.CheckPredicateFields(GetUsedScopeFields(callInfo), node, GetLambdaParamNames(callInfo.ScopeNest + 1), ErrorContainer); + func.ScopeInfo.CheckPredicateFields(this, callInfo); + } } public void CheckAndMarkAsPageable(FirstNameNode node) @@ -2050,10 +2065,18 @@ public DType GetUsedScopeFields(CallInfo call) foreach (var name in GetLambdaParamNames(call.ScopeNest + 1)) { - var fError = false; + var fError = false; + var fieldName = name.Name; + var foundLogicalName = call.CursorType.TryGetLogicalName(name.Name, out var logicalName); + + if (foundLogicalName) + { + fieldName = logicalName; + } + if (!name.Node.InTree(arg0) && - name.Node.InTree(call.Node) && - call.CursorType.TryGetType(name.Name, out var lambdaParamType)) + name.Node.InTree(call.Node) && + call.CursorType.TryGetType(fieldName, out var lambdaParamType)) { if (name.Node.Parent is DottedNameNode dotted) { @@ -3114,8 +3137,8 @@ private bool IsRowScopeField(FirstNameNode node, out Scope scope, out bool fErro scope = default; return false; } - - var nodeName = node.Ident.Name; + + var nodeName = node.Ident.Name; // Look up the name in the current scopes, innermost to outermost. // The logic here is as follows: @@ -3137,13 +3160,12 @@ private bool IsRowScopeField(FirstNameNode node, out Scope scope, out bool fErro // If scope type is a data source, the node may be a display name instead of logical. // Attempt to get the logical name to use for type checking. // If this is executed amidst a metadata refresh then the reference may refer to an old - // display name, so we need to check the old mapping as well as the current mapping. - var usesDisplayName = - DType.TryGetConvertedDisplayNameAndLogicalNameForColumn(scope.Type, nodeName.Value, out var maybeLogicalName, out _) || - DType.TryGetLogicalNameForColumn(scope.Type, nodeName.Value, out maybeLogicalName); - if (usesDisplayName) - { - nodeName = new DName(maybeLogicalName); + // display name, so we need to check the old mapping as well as the current mapping. + var foundLoficalName = scope.Type.TryGetLogicalName(node.Ident.Name, out var logicalName); + + if (foundLoficalName) + { + nodeName = logicalName; } if (scope.Type.TryGetType(nodeName, out var typeTmp)) @@ -4862,7 +4884,8 @@ private void FinalizeCall(CallNode node) } _txb.CheckAndMarkAsDelegatable(node); - _txb.CheckAndMarkAsPageable(node, func); + _txb.CheckAndMarkAsPageable(node, func); + _txb.CheckPredicateUsage(node, func); // A function will produce a constant output (and have no side-effects, which is important for // caching/precomputing the result) iff the function is pure and its arguments are constant. diff --git a/src/libraries/Microsoft.PowerFx.Core/Functions/FunctionScopeInfo.cs b/src/libraries/Microsoft.PowerFx.Core/Functions/FunctionScopeInfo.cs index 2a6f6fac84..0ab032ef76 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Functions/FunctionScopeInfo.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Functions/FunctionScopeInfo.cs @@ -2,8 +2,11 @@ // Licensed under the MIT license. using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.PowerFx.Core.App.ErrorContainers; using Microsoft.PowerFx.Core.Binding; +using Microsoft.PowerFx.Core.Binding.BindInfo; using Microsoft.PowerFx.Core.Errors; using Microsoft.PowerFx.Core.Localization; using Microsoft.PowerFx.Core.Types; @@ -39,7 +42,7 @@ internal class FunctionScopeInfo /// If false, the author will be warned when inputting predicates that /// do not reference the input table. /// - public bool AcceptsLiteralPredicates { get; } + public bool CheckPredicateUsage { get; } /// /// True indicates that the function performs some sort of iteration over @@ -72,7 +75,7 @@ public FunctionScopeInfo( TexlFunction function, bool usesAllFieldsInScope = true, bool supportsAsyncLambdas = true, - bool acceptsLiteralPredicates = true, + bool checkPredicateUsage = false, bool iteratesOverScope = true, DType scopeType = null, Func appliesToArgument = null, @@ -80,7 +83,7 @@ public FunctionScopeInfo( { UsesAllFieldsInScope = usesAllFieldsInScope; SupportsAsyncLambdas = supportsAsyncLambdas; - AcceptsLiteralPredicates = acceptsLiteralPredicates; + CheckPredicateUsage = checkPredicateUsage; IteratesOverScope = iteratesOverScope; ScopeType = scopeType; _function = function; @@ -192,29 +195,6 @@ public virtual bool CheckInput(Features features, TexlNode inputNode, DType inpu public virtual bool CheckInput(Features features, CallNode callNode, TexlNode inputNode, DType inputSchema, out DType typeScope) { return CheckInput(features, inputNode, inputSchema, TexlFunction.DefaultErrorContainer, out typeScope); - } - - public void CheckLiteralPredicates(TexlNode[] args, IErrorContainer errors) - { - Contracts.AssertValue(args); - Contracts.AssertValue(errors); - - if (!AcceptsLiteralPredicates) - { - for (var i = 0; i < args.Length; i++) - { - if (_function.IsLambdaParam(args[i], i)) - { - if (args[i].Kind == NodeKind.BoolLit || - args[i].Kind == NodeKind.NumLit || - args[i].Kind == NodeKind.DecLit || - args[i].Kind == NodeKind.StrLit) - { - errors.EnsureError(DocumentErrorSeverity.Warning, args[i], TexlStrings.WarnLiteralPredicate); - } - } - } - } } /// @@ -235,10 +215,54 @@ public virtual bool GetScopeIdent(TexlNode[] nodes, out DName[] scopeIdents) } return false; + } + + public virtual void CheckPredicateFields(TexlBinding binding, CallInfo callInfo) + { + var fields = binding.GetUsedScopeFields(callInfo); + var lambdaParamNames = binding.GetLambdaParamNames(callInfo.ScopeNest + 1); + + if (fields == DType.Error || fields.GetAllNames(DPath.Root).Any()) + { + return; + } + + GetScopeIdent(callInfo.Node.Args.ChildNodes.ToArray(), out var idents); + + if (!lambdaParamNames.Any(lambdaName => idents.Contains(lambdaName.Name))) + { + binding.ErrorContainer.EnsureError(DocumentErrorSeverity.Warning, callInfo.Node, TexlStrings.WarnCheckPredicateUsage); + } } } - internal class FunctionThisGroupScopeInfo : FunctionScopeInfo + internal class FunctionFilterScopeInfo : FunctionScopeInfo + { + public FunctionFilterScopeInfo(TexlFunction function) + : base(function, checkPredicateUsage: true) + { + } + + public override void CheckPredicateFields(TexlBinding binding, CallInfo callInfo) + { + // Filter can also accept a view as argument. + // In the event of any argN (where N > 1) is a view, the binder will validate if the view is valid or not. + // We check if there is any view as argument. If there is, we just skip the predicate checking. + foreach (var childNode in callInfo.Node.Args.ChildNodes) + { + var argType = binding.GetType(childNode); + + if (argType.Kind == DKind.ViewValue) + { + return; + } + } + + base.CheckPredicateFields(binding, callInfo); + } + } + + internal class FunctionThisGroupScopeInfo : FunctionScopeInfo { public static DName ThisGroup => new DName("ThisGroup"); @@ -283,14 +307,14 @@ public override bool CheckInput(Features features, CallNode callNode, TexlNode i } } - internal class FunctionJoinScopeInfo : FunctionScopeInfo + internal class FunctionJoinScopeInfo : FunctionScopeInfo { public static DName LeftRecord => new DName("LeftRecord"); public static DName RightRecord => new DName("RightRecord"); public FunctionJoinScopeInfo(TexlFunction function) - : base(function, appliesToArgument: (argIndex) => argIndex > 1) + : base(function, appliesToArgument: (argIndex) => argIndex > 1, checkPredicateUsage: true) { } @@ -324,7 +348,7 @@ public override bool GetScopeIdent(TexlNode[] nodes, out DName[] scopeIdents) { scopeIdents[0] = LeftRecord; } - + if (nodes.Length > 1 && nodes[1] is AsNode rightAsNode) { scopeIdents[1] = rightAsNode.Right.Name; @@ -332,11 +356,57 @@ public override bool GetScopeIdent(TexlNode[] nodes, out DName[] scopeIdents) else { scopeIdents[1] = RightRecord; - } + } // Returning false to indicate that the scope is not a whole scope. - // Meaning that the scope is a record type and we are accessing the fields directly. + // Meaning that the scope is a record type and we are accessing the fields directly. return false; } + + public override void CheckPredicateFields(TexlBinding binding, CallInfo callInfo) + { + var fields = binding.GetUsedScopeFields(callInfo); + var lambdaParamNames = binding.GetLambdaParamNames(callInfo.ScopeNest + 1); + + // If Join call node has less than 5 records, we are possibly looking for suggestions. + if (callInfo.Node.Args.ChildNodes.Count < 5 || fields == DType.Error) + { + return; + } + + GetScopeIdent(callInfo.Node.Args.ChildNodes.ToArray(), out var idents); + + var foundIdents = new HashSet(); + var predicate = callInfo.Node.Args.ChildNodes[2]; + + // In the Join function, arg2 and argN > 3 are lambdas nodes. + // We need to check if scope identifiers are used arg2 (predicate). + foreach (var lambda in lambdaParamNames) + { + var parent = lambda.Node.Parent; + + while (parent != null) + { + if (parent.Id == predicate.Id) + { + foundIdents.Add(lambda.Name); + break; + } + + parent = parent.Parent; + } + } + + var foundIdentsArray = foundIdents.ToArray(); + + if (foundIdents.Count == 2 && + fields.TryGetType(foundIdentsArray[0], out _) && + fields.TryGetType(foundIdentsArray[1], out _)) + { + return; + } + + binding.ErrorContainer.EnsureError(DocumentErrorSeverity.Warning, callInfo.Node, TexlStrings.WarnCheckPredicateUsage); + } } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Functions/TexlFunction.cs b/src/libraries/Microsoft.PowerFx.Core/Functions/TexlFunction.cs index e739d8dabf..477b26cc60 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Functions/TexlFunction.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Functions/TexlFunction.cs @@ -609,8 +609,6 @@ private bool CheckTypesCore(CheckTypesContext context, TexlNode[] args, DType[] nodeToCoercedTypeMap = null; } - ScopeInfo?.CheckLiteralPredicates(args, errors); - // Default return type. returnType = ReturnType; diff --git a/src/libraries/Microsoft.PowerFx.Core/IR/IRTranslator.cs b/src/libraries/Microsoft.PowerFx.Core/IR/IRTranslator.cs index 13d7117790..11925f02fc 100644 --- a/src/libraries/Microsoft.PowerFx.Core/IR/IRTranslator.cs +++ b/src/libraries/Microsoft.PowerFx.Core/IR/IRTranslator.cs @@ -616,7 +616,7 @@ private static IntermediateNode ConcatenateArgs(IntermediateNode arg1, Intermedi } } - var concatenatedNode = new CallNode(irContext, BuiltinFunctionsCore.Concatenate, concatenateArgs); + var concatenatedNode = new CallNode(irContext, BuiltinFunctionsCore.Concatenate, concatenateArgs); return concatenatedNode; } diff --git a/src/libraries/Microsoft.PowerFx.Core/Localization/Strings.cs b/src/libraries/Microsoft.PowerFx.Core/Localization/Strings.cs index d85e067f2d..ed92b0c32d 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Localization/Strings.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Localization/Strings.cs @@ -804,7 +804,7 @@ internal static class TexlStrings public static ErrorResourceKey ErrAsNotInContext = new ErrorResourceKey("ErrAsNotInContext"); public static ErrorResourceKey ErrValueMustBeFullyQualified = new ErrorResourceKey("ErrValueMustBeFullyQualified"); public static ErrorResourceKey WarnColumnNameSpecifiedMultipleTimes_Name = new ErrorResourceKey("WarnColumnNameSpecifiedMultipleTimes_Name"); - public static ErrorResourceKey WarnLiteralPredicate = new ErrorResourceKey("WarnLiteralPredicate"); + public static ErrorResourceKey WarnCheckPredicateUsage = new ErrorResourceKey("WarnCheckPredicateUsage"); public static ErrorResourceKey WarnDynamicMetadata = new ErrorResourceKey("WarnDynamicMetadata"); public static ErrorResourceKey WarnDeferredType = new ErrorResourceKey("WarnDeferredType"); public static ErrorResourceKey ErrColRenamedTwice_Name = new ErrorResourceKey("ErrColRenamedTwice_Name"); diff --git a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Filter.cs b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Filter.cs index 09fc51746e..e9a62f0f8f 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Filter.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Filter.cs @@ -27,7 +27,7 @@ internal sealed class FilterFunction : FilterFunctionBase public FilterFunction() : base("Filter", TexlStrings.AboutFilter, FunctionCategories.Table, DType.EmptyTable, -2, 2, int.MaxValue, DType.EmptyTable) { - ScopeInfo = new FunctionScopeInfo(this, acceptsLiteralPredicates: false); + ScopeInfo = new FunctionFilterScopeInfo(this); } public override IEnumerable GetSignatures() @@ -145,7 +145,7 @@ public override bool IsServerDelegatable(CallNode callNode, TexlBinding binding) Contracts.AssertValue(callNode); Contracts.AssertValue(binding); - if (!CheckArgsCount(callNode, binding)) + if (!CheckArgsCount(callNode, binding, DocumentErrorSeverity.Moderate)) { return false; } diff --git a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Lookup.cs b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Lookup.cs index 2440d591c9..1929b7ef0a 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Lookup.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Lookup.cs @@ -25,7 +25,7 @@ internal sealed class LookUpFunction : FilterFunctionBase public LookUpFunction() : base("LookUp", TexlStrings.AboutLookUp, FunctionCategories.Table, DType.Unknown, 0x6, 2, 3, DType.EmptyTable, DType.Boolean) { - ScopeInfo = new FunctionScopeInfo(this); + ScopeInfo = new FunctionScopeInfo(this, checkPredicateUsage: true); } public override IEnumerable GetSignatures() diff --git a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/StatisticalTableFunction.cs b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/StatisticalTableFunction.cs index 98a6397e78..6eecdb2124 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/StatisticalTableFunction.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/StatisticalTableFunction.cs @@ -27,7 +27,7 @@ internal abstract class StatisticalTableFunction : FunctionWithTableInput public StatisticalTableFunction(string name, TexlStrings.StringGetter description, FunctionCategories fc, bool nativeDecimal = false) : base(name, description, fc, DType.Number, 0x02, 2, 2, DType.EmptyTable, DType.Number) { - ScopeInfo = new FunctionScopeInfo(this, usesAllFieldsInScope: false, acceptsLiteralPredicates: false); + ScopeInfo = new FunctionScopeInfo(this, usesAllFieldsInScope: false, checkPredicateUsage: true); _nativeDecimal = nativeDecimal; } @@ -116,8 +116,6 @@ public override bool CheckTypes(CheckTypesContext context, TexlNode[] args, DTyp return false; } - ScopeInfo?.CheckLiteralPredicates(args, errors); - return true; } } diff --git a/src/libraries/Microsoft.PowerFx.Core/Types/DTypeExtensionsCore.cs b/src/libraries/Microsoft.PowerFx.Core/Types/DTypeExtensionsCore.cs index 2837e17c0a..beb3becc22 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Types/DTypeExtensionsCore.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Types/DTypeExtensionsCore.cs @@ -81,5 +81,28 @@ public static bool ContainsDataEntityType(this DType self, DPath path, int curre return self.GetNames(path).Any(n => n.Type.IsExpandEntity || (n.Type.IsAggregate && n.Type.ContainsDataEntityType(DPath.Root, currentDepth - 1))); } + + /// + /// Try to get the column logical name. + /// + /// + /// + /// + /// + public static bool TryGetLogicalName(this DType self, DName displayName, out DName logicalName) + { + var usesDisplayName = + DType.TryGetConvertedDisplayNameAndLogicalNameForColumn(self, displayName.Value, out var maybeLogicalName, out _) || + DType.TryGetLogicalNameForColumn(self, displayName.Value, out maybeLogicalName); + + logicalName = default; + + if (usesDisplayName) + { + logicalName = new DName(maybeLogicalName); + } + + return usesDisplayName; + } } } diff --git a/src/libraries/Microsoft.PowerFx.Interpreter/RecalcEngine.cs b/src/libraries/Microsoft.PowerFx.Interpreter/RecalcEngine.cs index f85f9ad6e4..c3035622b8 100644 --- a/src/libraries/Microsoft.PowerFx.Interpreter/RecalcEngine.cs +++ b/src/libraries/Microsoft.PowerFx.Interpreter/RecalcEngine.cs @@ -8,17 +8,14 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.PowerFx.Core; using Microsoft.PowerFx.Core.Binding; using Microsoft.PowerFx.Core.Errors; using Microsoft.PowerFx.Core.Functions; using Microsoft.PowerFx.Core.Glue; using Microsoft.PowerFx.Core.Parser; -using Microsoft.PowerFx.Core.Types; using Microsoft.PowerFx.Core.Utils; using Microsoft.PowerFx.Functions; using Microsoft.PowerFx.Interpreter; -using Microsoft.PowerFx.Syntax; using Microsoft.PowerFx.Types; namespace Microsoft.PowerFx diff --git a/src/strings/PowerFxResources.en-US.resx b/src/strings/PowerFxResources.en-US.resx index d31cbffae9..7629fa312e 100644 --- a/src/strings/PowerFxResources.en-US.resx +++ b/src/strings/PowerFxResources.en-US.resx @@ -2439,9 +2439,9 @@ A column named '{0}' was specified more than once. Duplicate columns. - - Warning: This predicate is a literal value and does not reference the input table. - Warning given when a literal predicate is given to a function operating over a table. + + Warning: The expression does not access scope fields directly and can return unexpected results. + Warning given when an expression with a function operating over a table does not access the predicate directly. Warning: Select "Capture Schema" at the bottom of the expanded formula bar to set and refresh this method's result schema. Otherwise this method will return no result. diff --git a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/AssociatedDataSourcesTests/TestDelegationValidation.cs b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/AssociatedDataSourcesTests/TestDelegationValidation.cs index ebdc59df63..d012963ccc 100644 --- a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/AssociatedDataSourcesTests/TestDelegationValidation.cs +++ b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/AssociatedDataSourcesTests/TestDelegationValidation.cs @@ -149,7 +149,7 @@ public void TestCountRowsWarningForCachedData(bool isCachedData) // Only shows warning if data source is passed directly to CountRows result = engine.Check("CountRows(Filter(Accounts, IsBlank('Address 1: City')))"); - Assert.True(result.IsSuccess); + Assert.True(result.IsSuccess); Assert.Empty(result.Errors); } } diff --git a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/Helpers/TestUtils.cs b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/Helpers/TestUtils.cs index 3f30ef8ea8..d0e5d63aa9 100644 --- a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/Helpers/TestUtils.cs +++ b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/Helpers/TestUtils.cs @@ -284,7 +284,7 @@ public MockSilentDelegableFilterFunction(string name, string runtimeFunctionName : base(name, (l) => "MockFunction", FunctionCategories.Table, DType.EmptyTable, -2, 2, int.MaxValue, DType.EmptyTable) { _runtimeFunctionNameSuffix = runtimeFunctionNameSuffix; - ScopeInfo = new FunctionScopeInfo(this, acceptsLiteralPredicates: false); + ScopeInfo = new FunctionScopeInfo(this, checkPredicateUsage: false); } public override bool IsServerDelegatable(CallNode callNode, TexlBinding binding) diff --git a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/TexlTests.cs b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/TexlTests.cs index 40e113ac60..6316c2a51e 100644 --- a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/TexlTests.cs +++ b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/TexlTests.cs @@ -4546,6 +4546,61 @@ public void TestTypeLiteralsNegative(string script, string expectedSchema) features: Features.PowerFxV1); } + [Theory] + [InlineData("Filter(t1, a = 1)", false)] + [InlineData("Filter(t1, Abs(a) = 1)", false)] + [InlineData("Sum(t1, a)", false)] + [InlineData("Average(t1, a)", false)] + [InlineData("Min(t1, a)", false)] + [InlineData("Max(t1, a)", false)] + [InlineData("Join(t1, t2, LeftRecord.a = RightRecord.x, JoinType.Inner, RightRecord.z As Z)", false)] + [InlineData("Join(t1 As X, t2 As Y, X.a = Y.x, JoinType.Inner, Y.z As Z)", false)] + [InlineData("Summarize(t1, a)", false)] + [InlineData("Summarize(t1, a, CountRows(ThisGroup) As Counter)", false)] + [InlineData("LookUp(t1, a = 1)", false)] + [InlineData("Max(t1, Abs(a))", false)] + [InlineData("Min(t1, Abs(a))", false)] + [InlineData("Average(t1, Abs(a))", false)] + [InlineData("Sum(t1, Abs(a))", false)] + [InlineData("Filter(t1, With({x:a}, StartsWith(x, \"something\")))", false)] + [InlineData("Filter(t1 As Tbl, With({b:\"hello\"}, StartsWith(Tbl.b, b)))", false)] + [InlineData("If(LookUp(t1, a = 1).a = 1, 1)", false)] + + [InlineData("Filter(t1, 1=1)", true)] + [InlineData("Filter(t1, With({b:\"hello\"}, StartsWith(b, \"something\")))", true)] + [InlineData("Sum(t1, 1)", true)] + [InlineData("Average(t1, 1)", true)] + [InlineData("Min(t1, 1)", true)] + [InlineData("Max(t1, 1)", true)] + [InlineData("Join(t1, t2, true, JoinType.Inner, RightRecord.z As Z, LeftRecord.a As AAA)", true)] + [InlineData("LookUp(t1, 1=1)", true)] + [InlineData("LookUp(t1, With({a:99}, a) = 99)", true)] + + public void TestScopePredicate(string expression, bool expectingWarning) + { + var symbolTable = new SymbolTable(); + + symbolTable.AddVariable("t1", new TableType(TestUtils.DT("*[a:w,b:s,c:b]"))); + symbolTable.AddVariable("t2", new TableType(TestUtils.DT("*[x:w,y:s,z:b]"))); + symbolTable.AddFunction(new JoinFunction()); + symbolTable.AddFunction(new SummarizeFunction()); + + var engine = new Engine(); + var check = engine.Check(expression, symbolTable: symbolTable); + + // The predicate checking should never fail, but it may produce a warning. + Assert.True(check.IsSuccess, string.Join("\n", check.Errors.Select(err => err.ToString()))); + + if (expectingWarning) + { + Assert.Contains(check.Errors, err => err.MessageKey == "WarnCheckPredicateUsage"); + } + else + { + Assert.DoesNotContain(check.Errors, err => err.MessageKey == "WarnCheckPredicateUsage"); + } + } + private void TestBindingPurity(string script, bool isPure, SymbolTable symbolTable = null) { var config = new PowerFxConfig diff --git a/src/tests/Microsoft.PowerFx.TexlFunctionExporter.Shared/YamlTexlFunction.cs b/src/tests/Microsoft.PowerFx.TexlFunctionExporter.Shared/YamlTexlFunction.cs index 14a5c547ff..1fb4162430 100644 --- a/src/tests/Microsoft.PowerFx.TexlFunctionExporter.Shared/YamlTexlFunction.cs +++ b/src/tests/Microsoft.PowerFx.TexlFunctionExporter.Shared/YamlTexlFunction.cs @@ -491,7 +491,7 @@ public YamlTexlScopeInfo() internal YamlTexlScopeInfo(FunctionScopeInfo scopeInfo) { - AcceptsLiteralPredicates = scopeInfo.AcceptsLiteralPredicates; + AcceptsLiteralPredicates = scopeInfo.CheckPredicateUsage; CanBeCreatedByRecord = scopeInfo.CanBeCreatedByRecord; HasNondeterministicOperationOrder = scopeInfo.HasNondeterministicOperationOrder; IteratesOverScope = scopeInfo.IteratesOverScope;