Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,7 @@ public void CheckLiteralPredicates(TexlNode[] args, IErrorContainer errors)
{
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)
if (IsConstantExpression(args[i]))
{
errors.EnsureError(DocumentErrorSeverity.Warning, args[i], TexlStrings.WarnLiteralPredicate);
}
Expand All @@ -217,6 +214,45 @@ public void CheckLiteralPredicates(TexlNode[] args, IErrorContainer errors)
}
}

/// <summary>
/// Returns true if the given node is a constant expression — i.e. the entire subtree
/// contains no FirstName or DottedName references that could point to a scope symbol
/// or any other named value. Bare literals, and unary/binary ops whose operands are
/// all constant, are considered constant. Call nodes are conservatively treated as
/// non-constant to avoid false positives on functions with external state.
/// </summary>
internal static bool IsConstantExpression(TexlNode node)
{
Contracts.AssertValue(node);

switch (node.Kind)
{
case NodeKind.BoolLit:
case NodeKind.NumLit:
case NodeKind.DecLit:
case NodeKind.StrLit:
case NodeKind.Blank:
return true;

case NodeKind.UnaryOp:
var unary = node.AsUnaryOpLit();
return unary != null && IsConstantExpression(unary.Child);

case NodeKind.BinaryOp:
var binary = node.AsBinaryOp();
return binary != null && IsConstantExpression(binary.Left) && IsConstantExpression(binary.Right);

// FirstName / DottedName always reference a symbol — not constant.
case NodeKind.FirstName:
case NodeKind.DottedName:
return false;

default:
// All other node kinds (Call, Record, Table, etc.) are not constant.
return false;
}
}

/// <summary>
/// Get the scope identifiers for the function based on the CallNode child args.
/// The majority of the functions will have a single scope identifier named ThisRecord.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,7 @@ internal static class TexlStrings
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 WarnJoinPredicateDoesNotUseScope = new ErrorResourceKey("WarnJoinPredicateDoesNotUseScope");
public static ErrorResourceKey WarnDynamicMetadata = new ErrorResourceKey("WarnDynamicMetadata");
public static ErrorResourceKey WarnDeferredType = new ErrorResourceKey("WarnDeferredType");
public static ErrorResourceKey ErrColRenamedTwice_Name = new ErrorResourceKey("ErrColRenamedTwice_Name");
Expand Down
65 changes: 65 additions & 0 deletions src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/Join.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.PowerFx.Core.App.ErrorContainers;
using Microsoft.PowerFx.Core.Binding;
using Microsoft.PowerFx.Core.Errors;
using Microsoft.PowerFx.Core.Functions;
using Microsoft.PowerFx.Core.IR;
Expand Down Expand Up @@ -148,6 +149,70 @@ public override bool CheckTypes(CheckTypesContext context, TexlNode[] args, DTyp
return valid;
}

public override void CheckSemantics(TexlBinding binding, TexlNode[] args, DType[] argTypes, IErrorContainer errors)
{
Contracts.AssertValue(binding);
Contracts.AssertValue(args);
Contracts.AssertValue(errors);

base.CheckSemantics(binding, args, argTypes, errors);

// Warn when the predicate (arg index 2) does not reference one of the two scope symbols.
// args.Length must be at least 3 to have a predicate argument.
if (args.Length >= 3)
{
var predicate = args[2];
ScopeInfo.GetScopeIdent(args, out var scopeIdents);

for (var s = 0; s < scopeIdents.Length; s++)
{
if (!PredicateReferencesScope(predicate, scopeIdents[s]))
{
errors.EnsureError(DocumentErrorSeverity.Warning, predicate, TexlStrings.WarnJoinPredicateDoesNotUseScope, scopeIdents[s]);
}
}
}
}

/// <summary>
/// Returns true if the predicate AST subtree contains at least one FirstName or DottedName
/// node whose root FirstName identifier equals <paramref name="scopeIdent"/>.
/// </summary>
private static bool PredicateReferencesScope(TexlNode node, DName scopeIdent)
{
Contracts.AssertValue(node);

switch (node.Kind)
{
case NodeKind.FirstName:
return node.AsFirstName().Ident.Name == scopeIdent;

case NodeKind.DottedName:
// For dotted names like T1.a, we check whether the root (leftmost) FirstName
// matches the scope identifier.
return PredicateReferencesScope(node.AsDottedName().Left, scopeIdent);

case NodeKind.BinaryOp:
var binary = node.AsBinaryOp();
return PredicateReferencesScope(binary.Left, scopeIdent) ||
PredicateReferencesScope(binary.Right, scopeIdent);

case NodeKind.UnaryOp:
return PredicateReferencesScope(node.AsUnaryOpLit().Child, scopeIdent);

case NodeKind.Call:
var call = node.AsCall();
return call.Args.Children.Any(child => PredicateReferencesScope(child, scopeIdent));

case NodeKind.VariadicOp:
var variadic = node.AsVariadicOp();
return variadic.ChildNodes.Any(child => PredicateReferencesScope(child, scopeIdent));

default:
return false;
}
}

public override bool IsLambdaParam(TexlNode node, int index)
{
return index == 2 || index > 3;
Expand Down
4 changes: 4 additions & 0 deletions src/strings/PowerFxResources.en-US.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2451,6 +2451,10 @@
<value>Warning: This predicate is a literal value and does not reference the input table.</value>
<comment>Warning given when a literal predicate is given to a function operating over a table.</comment>
</data>
<data name="WarnJoinPredicateDoesNotUseScope" xml:space="preserve">
<value>Warning: The Join predicate does not reference '{0}'. Every row from that side will match, which is likely unintentional.</value>
<comment>Warning given when a Join predicate does not reference one of the two scope symbols (LeftRecord/RightRecord or their aliases).</comment>
</data>
<data name="WarnDynamicMetadata" xml:space="preserve">
<value>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.</value>
<comment>Warning given when service function returns dynamic metadata.</comment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,59 @@ public void CheckSuccessWarning()
Assert.Equal(1, result.Errors.Count(x => x.Severity == ErrorSeverity.Warning));
}

// Issue #2795: Filter should warn on compound constant predicates (not just bare literals)
[Fact]
public void FilterCompoundConstantPredicateWarning()
{
var engine = new RecalcEngine();

// "true && true" is a compound constant — no record fields are referenced.
// Currently produces NO warning; after the fix it MUST produce exactly one Warning.
var result = engine.Check("Filter([10,20,30], true && true)");

Assert.True(result.IsSuccess);
Assert.Equal(1, result.Errors.Count(x => x.Severity == ErrorSeverity.Warning));
}

// Issue #2795: Join should warn when the predicate references only one scope symbol
[Fact]
public void JoinOneSidedPredicateWarning()
{
var config = new PowerFxConfig(Features.PowerFxV1);
#pragma warning disable CS0618 // Join is in preview
config.EnableJoinFunction();
#pragma warning restore CS0618

var engine = new RecalcEngine(config);

// Predicate only uses LeftRecord (T1.a) — T2 is never referenced.
// Currently produces NO warning; after the fix it MUST produce exactly one Warning.
var result = engine.Check(
"Join([{a:1,b:2}] As T1, [{a:1,c:99}] As T2, T1.a > 0, JoinType.Left, T2.c As CC)");

Assert.True(result.IsSuccess);
Assert.Equal(1, result.Errors.Count(x => x.Severity == ErrorSeverity.Warning));
}

// Issue #2795: Join should NOT warn when predicate uses both LeftRecord and RightRecord
[Fact]
public void JoinBothSidesPredicateNoWarning()
{
var config = new PowerFxConfig(Features.PowerFxV1);
#pragma warning disable CS0618 // Join is in preview
config.EnableJoinFunction();
#pragma warning restore CS0618

var engine = new RecalcEngine(config);

// Predicate references both LeftRecord (T1) and RightRecord (T2) — no warning expected.
var result = engine.Check(
"Join([{a:1,b:2}] As T1, [{a:1,c:99}] As T2, T1.a = T2.a, JoinType.Inner, T2.c As CC)");

Assert.True(result.IsSuccess);
Assert.Equal(0, result.Errors.Count(x => x.Severity == ErrorSeverity.Warning));
}

[Fact]
public void CheckParseError()
{
Expand Down
Loading