Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions optimizer/src/main/java/dev/cel/optimizer/AstMutator.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -664,8 +664,8 @@ private CelMutableSource mangleIdentsInMacroSource(
return newSource;
}

private static CelMutableSource combine(
CelMutableSource celSource1, CelMutableSource celSource2) {
/** Combines two {@link CelMutableSource} instances into a single new instance. */
public static CelMutableSource combine(CelMutableSource celSource1, CelMutableSource celSource2) {
return CelMutableSource.newInstance()
.setDescription(
Strings.isNullOrEmpty(celSource1.getDescription())
Expand All@@ -677,6 +677,7 @@ private static CelMutableSource combine(
.addAllMacroCalls(celSource2.getMacroCalls());
}


/**
* Stabilizes the incoming AST by ensuring that all of expr IDs are consistently renumbered
* (monotonically increased) from the starting seed ID. If the AST contains any macro calls, its
Expand Down
4 changes: 4 additions & 0 deletions policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ java_library(
name = "compiled_rule",
srcs = ["CelCompiledRule.java"],
deps = [
":policy",
"//:auto_value",
"//bundle:cel",
"//common:cel_ast",
Expand DownExpand Up@@ -246,6 +247,7 @@ java_library(
srcs = ["RuleComposer.java"],
deps = [
":compiled_rule",
":policy",
"//bundle:cel",
"//common:cel_ast",
"//common:compiler_common",
Expand All@@ -256,11 +258,13 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
"@maven//:com_google_guava_guava",
"@maven//:org_jspecify_jspecify",
],
)
15 changes: 13 additions & 2 deletions policy/src/main/java/dev/cel/policy/CelCompiledRule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@
import dev.cel.common.ast.CelConstant;
import dev.cel.common.ast.CelExpr;
import dev.cel.common.formats.ValueString;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import java.util.Optional;

/**
Expand All@@ -43,11 +44,20 @@ public abstract class CelCompiledRule {

public abstract Cel cel();

public abstract EvaluationSemantic semantic();

/**
* HasOptionalOutput returns whether the rule returns a concrete or optional value. The rule may
* return an optional value if all match expressions under the rule are conditional.
*/
public boolean hasOptionalOutput() {
// AGGREGATE rules always return a concrete list (falling back to an empty list rather than
// optional.none()), meaning they are never optional structurally. This also prevents dead
// code evasion inside parent FIRST_MATCH rules.
if (semantic() == EvaluationSemantic.AGGREGATE) {
return false;
}

boolean isOptionalOutput = false;
for (CelCompiledMatch match : matches()) {
if (match.result().kind().equals(CelCompiledMatch.Result.Kind.RULE)
Expand DownExpand Up@@ -157,7 +167,8 @@ static CelCompiledRule create(
Optional<ValueString> ruleId,
ImmutableList<CelCompiledVariable> variables,
ImmutableList<CelCompiledMatch> matches,
Cel cel) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel);
Cel cel,
CelPolicy.EvaluationSemantic semantic) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel, semantic);
}
}
13 changes: 12 additions & 1 deletion policy/src/main/java/dev/cel/policy/CelPolicy.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@
@AutoValue
public abstract class CelPolicy {

/** Evaluation semantic for a rule. */
public enum EvaluationSemantic {
FIRST_MATCH,
AGGREGATE
}

public abstract ValueString name();

public abstract Optional<ValueString> description();
Expand DownExpand Up@@ -176,12 +182,15 @@ public abstract static class Rule {

public abstract ImmutableSet<Match> matches();

public abstract EvaluationSemantic semantic();

/** Builder for {@link Rule}. */
public static Builder newBuilder(long id) {
return new AutoValue_CelPolicy_Rule.Builder()
.setId(id)
.setVariables(ImmutableSet.of())
.setMatches(ImmutableSet.of());
.setMatches(ImmutableSet.of())
.setSemantic(EvaluationSemantic.FIRST_MATCH);
}

/** Creates a new builder to construct a {@link Rule} instance. */
Expand DownExpand Up@@ -228,6 +237,8 @@ public Builder addMatches(Iterable<Match> matches) {

abstract Rule.Builder setMatches(ImmutableSet<Match> matches);

public abstract Rule.Builder setSemantic(EvaluationSemantic semantic);

public abstract Rule build();
}
}
Expand Down
41 changes: 36 additions & 5 deletions policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result.Kind;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Match;
import dev.cel.policy.CelPolicy.Variable;
Expand DownExpand Up@@ -91,7 +92,8 @@ public CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationE
extendedCel = extendedCel.toCelBuilder().setContainer(containerBuilder.build()).build();
}

CelCompiledRule compiledRule = compileRuleImpl(policy.rule(), extendedCel, compilerContext);
CelCompiledRule compiledRule =
compileRuleImpl(policy.rule(), extendedCel, compilerContext, false);
if (compilerContext.hasError()) {
throw new CelPolicyValidationException(compilerContext.getIssueString());
}
Expand DownExpand Up@@ -172,7 +174,14 @@ private void assertAstDepthIsSafe(CelAbstractSyntaxTree ast, Cel cel)
}

private CelCompiledRule compileRuleImpl(
CelPolicy.Rule rule, Cel ruleCel, CompilerContext compilerContext) {
CelPolicy.Rule rule,
Cel ruleCel,
CompilerContext compilerContext,
boolean hasAggregateAncestor) {
if (hasAggregateAncestor && rule.semantic().equals(EvaluationSemantic.AGGREGATE)) {
compilerContext.addIssue(
rule.id(), CelIssue.formatError(1, 0, "nested aggregate rules are not allowed"));
}
// A local CEL environment used to compile a single rule. This temporary environment
// is used to declare policy variables iteratively in a given policy, ensuring proper scoping
// across a single / nested rule.
Expand DownExpand Up@@ -227,8 +236,11 @@ private CelCompiledRule compileRuleImpl(
matchResult = Result.ofOutput(output.id(), outputAst);
break;
case RULE:
boolean nextHasAggregateAncestor =
hasAggregateAncestor || rule.semantic().equals(EvaluationSemantic.AGGREGATE);
CelCompiledRule nestedRule =
compileRuleImpl(match.result().rule(), localCel, compilerContext);
compileRuleImpl(
match.result().rule(), localCel, compilerContext, nextHasAggregateAncestor);
matchResult = Result.ofRule(nestedRule);
break;
default:
Expand All@@ -240,7 +252,12 @@ private CelCompiledRule compileRuleImpl(

CelCompiledRule compiledRule =
CelCompiledRule.create(
rule.id(), rule.ruleId(), variableBuilder.build(), matchBuilder.build(), ruleCel);
rule.id(),
rule.ruleId(),
variableBuilder.build(),
matchBuilder.build(),
ruleCel,
rule.semantic());

// Validate that all branches in the policy are reachable
checkUnreachableCode(compiledRule, compilerContext);
Expand All@@ -255,6 +272,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
CelCompiledMatch compiledMatch = compiledMatches.get(i);
boolean isTriviallyTrue = compiledMatch.isConditionTriviallyTrue();

// Flag literally false conditions as dead code regardless of semantic
if (isConditionLiterallyFalse(compiledMatch.condition())) {
compilerContext.addIssue(
compiledMatch.sourceId(), CelIssue.formatError(1, 0, "Condition is always false"));
}

// If the match is a single output or a nested rule that always returns a value, it is
// exhaustive. If the condition is trivially true, then all subsequent branches are
// unreachable.
Expand All@@ -263,7 +286,9 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
&& (compiledMatch.result().kind().equals(Kind.OUTPUT)
|| !compiledMatch.result().rule().hasOptionalOutput());

if (isExhaustive && i != matchCount - 1) {
if (compiledRule.semantic() == EvaluationSemantic.FIRST_MATCH
&& isExhaustive
&& i != matchCount - 1) {
if (compiledMatch.result().kind().equals(Kind.OUTPUT)) {
compilerContext.addIssue(
compiledMatch.sourceId(),
Expand All@@ -277,6 +302,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
}
}

private static boolean isConditionLiterallyFalse(CelAbstractSyntaxTree condition) {
CelExpr celExpr = condition.getExpr();
return celExpr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)
&& !celExpr.constant().booleanValue();
}

private static CelAbstractSyntaxTree newErrorAst() {
return CelAbstractSyntaxTree.newParsedAst(
CelExpr.ofConstant(0, CelConstant.ofValue("*error*")), CelSource.newBuilder().build());
Expand Down
50 changes: 47 additions & 3 deletions policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@
import dev.cel.common.formats.YamlHelper.YamlNodeType;
import dev.cel.common.formats.YamlParserContextImpl;
import dev.cel.common.internal.CelCodePointArray;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Invariant;
import dev.cel.policy.CelPolicy.Match;
Expand DownExpand Up@@ -271,6 +272,8 @@ public CelPolicy.Rule parseRule(
return ruleBuilder.build();
}

boolean hasMatch = false;
boolean hasAggregate = false;
for (NodeTuple nodeTuple : ((MappingNode) node).getValue()) {
Node key = nodeTuple.getKeyNode();
long tagId = ctx.collectMetadata(key);
Expand All@@ -290,8 +293,24 @@ public CelPolicy.Rule parseRule(
ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value));
break;
case "match":
ruleBuilder.addMatches(parseMatches(ctx, policyBuilder, value));
if (hasAggregate) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasMatch = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, false))
.setSemantic(EvaluationSemantic.FIRST_MATCH);
break;
case "aggregate":
if (hasMatch) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasAggregate = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, true))
.setSemantic(EvaluationSemantic.AGGREGATE);
break;

default:
tagVisitor.visitRuleTag(ctx, tagId, fieldName, value, policyBuilder, ruleBuilder);
break;
Expand All@@ -301,7 +320,10 @@ public CelPolicy.Rule parseRule(
}

private ImmutableSet<CelPolicy.Match> parseMatches(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long valueId = ctx.collectMetadata(node);
ImmutableSet.Builder<CelPolicy.Match> matchesBuilder = ImmutableSet.builder();
if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
Expand All@@ -310,7 +332,7 @@ private ImmutableSet<CelPolicy.Match> parseMatches(

SequenceNode matchListNode = (SequenceNode) node;
for (Node elementNode : matchListNode.getValue()) {
matchesBuilder.add(parseMatch(ctx, policyBuilder, elementNode));
matchesBuilder.add(parseMatchInternal(ctx, policyBuilder, elementNode, isAggregate));
}

return matchesBuilder.build();
Expand All@@ -319,6 +341,14 @@ private ImmutableSet<CelPolicy.Match> parseMatches(
@Override
public CelPolicy.Match parseMatch(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
return parseMatchInternal(ctx, policyBuilder, node, false);
}

private CelPolicy.Match parseMatchInternal(
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long nodeId = ctx.collectMetadata(node);
if (!assertYamlType(ctx, nodeId, node, YamlNodeType.MAP)) {
return ERROR_MATCH;
Expand All@@ -339,6 +369,20 @@ public CelPolicy.Match parseMatch(
matchBuilder.setCondition(ctx.newSourceString(value));
break;
case "output":
if (isAggregate) {
ctx.reportError(tagId, "Rule aggregate requires 'emit' tag instead of 'output'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
.ifPresent(
result -> ctx.reportError(tagId, "Only the rule or the output may be set"));
matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value)));
break;
case "emit":
if (!isAggregate) {
ctx.reportError(tagId, "Rule match requires 'output' tag instead of 'emit'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions optimizer/src/main/java/dev/cel/optimizer/AstMutator.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -664,8 +664,8 @@ private CelMutableSource mangleIdentsInMacroSource(
return newSource;
}

private static CelMutableSource combine(
CelMutableSource celSource1, CelMutableSource celSource2) {
/** Combines two {@link CelMutableSource} instances into a single new instance. */
public static CelMutableSource combine(CelMutableSource celSource1, CelMutableSource celSource2) {
return CelMutableSource.newInstance()
.setDescription(
Strings.isNullOrEmpty(celSource1.getDescription())
Expand All@@ -677,6 +677,7 @@ private static CelMutableSource combine(
.addAllMacroCalls(celSource2.getMacroCalls());
}


/**
* Stabilizes the incoming AST by ensuring that all of expr IDs are consistently renumbered
* (monotonically increased) from the starting seed ID. If the AST contains any macro calls, its
Expand Down
4 changes: 4 additions & 0 deletions policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ java_library(
name = "compiled_rule",
srcs = ["CelCompiledRule.java"],
deps = [
":policy",
"//:auto_value",
"//bundle:cel",
"//common:cel_ast",
Expand DownExpand Up@@ -246,6 +247,7 @@ java_library(
srcs = ["RuleComposer.java"],
deps = [
":compiled_rule",
":policy",
"//bundle:cel",
"//common:cel_ast",
"//common:compiler_common",
Expand All@@ -256,11 +258,13 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
"@maven//:com_google_guava_guava",
"@maven//:org_jspecify_jspecify",
],
)
15 changes: 13 additions & 2 deletions policy/src/main/java/dev/cel/policy/CelCompiledRule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@
import dev.cel.common.ast.CelConstant;
import dev.cel.common.ast.CelExpr;
import dev.cel.common.formats.ValueString;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import java.util.Optional;

/**
Expand All@@ -43,11 +44,20 @@ public abstract class CelCompiledRule {

public abstract Cel cel();

public abstract EvaluationSemantic semantic();

/**
* HasOptionalOutput returns whether the rule returns a concrete or optional value. The rule may
* return an optional value if all match expressions under the rule are conditional.
*/
public boolean hasOptionalOutput() {
// AGGREGATE rules always return a concrete list (falling back to an empty list rather than
// optional.none()), meaning they are never optional structurally. This also prevents dead
// code evasion inside parent FIRST_MATCH rules.
if (semantic() == EvaluationSemantic.AGGREGATE) {
return false;
}

boolean isOptionalOutput = false;
for (CelCompiledMatch match : matches()) {
if (match.result().kind().equals(CelCompiledMatch.Result.Kind.RULE)
Expand DownExpand Up@@ -157,7 +167,8 @@ static CelCompiledRule create(
Optional<ValueString> ruleId,
ImmutableList<CelCompiledVariable> variables,
ImmutableList<CelCompiledMatch> matches,
Cel cel) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel);
Cel cel,
CelPolicy.EvaluationSemantic semantic) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel, semantic);
}
}
13 changes: 12 additions & 1 deletion policy/src/main/java/dev/cel/policy/CelPolicy.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@
@AutoValue
public abstract class CelPolicy {

/** Evaluation semantic for a rule. */
public enum EvaluationSemantic {
FIRST_MATCH,
AGGREGATE
}

public abstract ValueString name();

public abstract Optional<ValueString> description();
Expand DownExpand Up@@ -176,12 +182,15 @@ public abstract static class Rule {

public abstract ImmutableSet<Match> matches();

public abstract EvaluationSemantic semantic();

/** Builder for {@link Rule}. */
public static Builder newBuilder(long id) {
return new AutoValue_CelPolicy_Rule.Builder()
.setId(id)
.setVariables(ImmutableSet.of())
.setMatches(ImmutableSet.of());
.setMatches(ImmutableSet.of())
.setSemantic(EvaluationSemantic.FIRST_MATCH);
}

/** Creates a new builder to construct a {@link Rule} instance. */
Expand DownExpand Up@@ -228,6 +237,8 @@ public Builder addMatches(Iterable<Match> matches) {

abstract Rule.Builder setMatches(ImmutableSet<Match> matches);

public abstract Rule.Builder setSemantic(EvaluationSemantic semantic);

public abstract Rule build();
}
}
Expand Down
41 changes: 36 additions & 5 deletions policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result.Kind;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Match;
import dev.cel.policy.CelPolicy.Variable;
Expand DownExpand Up@@ -91,7 +92,8 @@ public CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationE
extendedCel = extendedCel.toCelBuilder().setContainer(containerBuilder.build()).build();
}

CelCompiledRule compiledRule = compileRuleImpl(policy.rule(), extendedCel, compilerContext);
CelCompiledRule compiledRule =
compileRuleImpl(policy.rule(), extendedCel, compilerContext, false);
if (compilerContext.hasError()) {
throw new CelPolicyValidationException(compilerContext.getIssueString());
}
Expand DownExpand Up@@ -172,7 +174,14 @@ private void assertAstDepthIsSafe(CelAbstractSyntaxTree ast, Cel cel)
}

private CelCompiledRule compileRuleImpl(
CelPolicy.Rule rule, Cel ruleCel, CompilerContext compilerContext) {
CelPolicy.Rule rule,
Cel ruleCel,
CompilerContext compilerContext,
boolean hasAggregateAncestor) {
if (hasAggregateAncestor && rule.semantic().equals(EvaluationSemantic.AGGREGATE)) {
compilerContext.addIssue(
rule.id(), CelIssue.formatError(1, 0, "nested aggregate rules are not allowed"));
}
// A local CEL environment used to compile a single rule. This temporary environment
// is used to declare policy variables iteratively in a given policy, ensuring proper scoping
// across a single / nested rule.
Expand DownExpand Up@@ -227,8 +236,11 @@ private CelCompiledRule compileRuleImpl(
matchResult = Result.ofOutput(output.id(), outputAst);
break;
case RULE:
boolean nextHasAggregateAncestor =
hasAggregateAncestor || rule.semantic().equals(EvaluationSemantic.AGGREGATE);
CelCompiledRule nestedRule =
compileRuleImpl(match.result().rule(), localCel, compilerContext);
compileRuleImpl(
match.result().rule(), localCel, compilerContext, nextHasAggregateAncestor);
matchResult = Result.ofRule(nestedRule);
break;
default:
Expand All@@ -240,7 +252,12 @@ private CelCompiledRule compileRuleImpl(

CelCompiledRule compiledRule =
CelCompiledRule.create(
rule.id(), rule.ruleId(), variableBuilder.build(), matchBuilder.build(), ruleCel);
rule.id(),
rule.ruleId(),
variableBuilder.build(),
matchBuilder.build(),
ruleCel,
rule.semantic());

// Validate that all branches in the policy are reachable
checkUnreachableCode(compiledRule, compilerContext);
Expand All@@ -255,6 +272,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
CelCompiledMatch compiledMatch = compiledMatches.get(i);
boolean isTriviallyTrue = compiledMatch.isConditionTriviallyTrue();

// Flag literally false conditions as dead code regardless of semantic
if (isConditionLiterallyFalse(compiledMatch.condition())) {
compilerContext.addIssue(
compiledMatch.sourceId(), CelIssue.formatError(1, 0, "Condition is always false"));
}

// If the match is a single output or a nested rule that always returns a value, it is
// exhaustive. If the condition is trivially true, then all subsequent branches are
// unreachable.
Expand All@@ -263,7 +286,9 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
&& (compiledMatch.result().kind().equals(Kind.OUTPUT)
|| !compiledMatch.result().rule().hasOptionalOutput());

if (isExhaustive && i != matchCount - 1) {
if (compiledRule.semantic() == EvaluationSemantic.FIRST_MATCH
&& isExhaustive
&& i != matchCount - 1) {
if (compiledMatch.result().kind().equals(Kind.OUTPUT)) {
compilerContext.addIssue(
compiledMatch.sourceId(),
Expand All@@ -277,6 +302,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
}
}

private static boolean isConditionLiterallyFalse(CelAbstractSyntaxTree condition) {
CelExpr celExpr = condition.getExpr();
return celExpr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)
&& !celExpr.constant().booleanValue();
}

private static CelAbstractSyntaxTree newErrorAst() {
return CelAbstractSyntaxTree.newParsedAst(
CelExpr.ofConstant(0, CelConstant.ofValue("*error*")), CelSource.newBuilder().build());
Expand Down
50 changes: 47 additions & 3 deletions policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@
import dev.cel.common.formats.YamlHelper.YamlNodeType;
import dev.cel.common.formats.YamlParserContextImpl;
import dev.cel.common.internal.CelCodePointArray;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Invariant;
import dev.cel.policy.CelPolicy.Match;
Expand DownExpand Up@@ -271,6 +272,8 @@ public CelPolicy.Rule parseRule(
return ruleBuilder.build();
}

boolean hasMatch = false;
boolean hasAggregate = false;
for (NodeTuple nodeTuple : ((MappingNode) node).getValue()) {
Node key = nodeTuple.getKeyNode();
long tagId = ctx.collectMetadata(key);
Expand All@@ -290,8 +293,24 @@ public CelPolicy.Rule parseRule(
ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value));
break;
case "match":
ruleBuilder.addMatches(parseMatches(ctx, policyBuilder, value));
if (hasAggregate) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasMatch = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, false))
.setSemantic(EvaluationSemantic.FIRST_MATCH);
break;
case "aggregate":
if (hasMatch) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasAggregate = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, true))
.setSemantic(EvaluationSemantic.AGGREGATE);
break;

default:
tagVisitor.visitRuleTag(ctx, tagId, fieldName, value, policyBuilder, ruleBuilder);
break;
Expand All@@ -301,7 +320,10 @@ public CelPolicy.Rule parseRule(
}

private ImmutableSet<CelPolicy.Match> parseMatches(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long valueId = ctx.collectMetadata(node);
ImmutableSet.Builder<CelPolicy.Match> matchesBuilder = ImmutableSet.builder();
if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
Expand All@@ -310,7 +332,7 @@ private ImmutableSet<CelPolicy.Match> parseMatches(

SequenceNode matchListNode = (SequenceNode) node;
for (Node elementNode : matchListNode.getValue()) {
matchesBuilder.add(parseMatch(ctx, policyBuilder, elementNode));
matchesBuilder.add(parseMatchInternal(ctx, policyBuilder, elementNode, isAggregate));
}

return matchesBuilder.build();
Expand All@@ -319,6 +341,14 @@ private ImmutableSet<CelPolicy.Match> parseMatches(
@Override
public CelPolicy.Match parseMatch(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
return parseMatchInternal(ctx, policyBuilder, node, false);
}

private CelPolicy.Match parseMatchInternal(
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long nodeId = ctx.collectMetadata(node);
if (!assertYamlType(ctx, nodeId, node, YamlNodeType.MAP)) {
return ERROR_MATCH;
Expand All@@ -339,6 +369,20 @@ public CelPolicy.Match parseMatch(
matchBuilder.setCondition(ctx.newSourceString(value));
break;
case "output":
if (isAggregate) {
ctx.reportError(tagId, "Rule aggregate requires 'emit' tag instead of 'output'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
.ifPresent(
result -> ctx.reportError(tagId, "Only the rule or the output may be set"));
matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value)));
break;
case "emit":
if (!isAggregate) {
ctx.reportError(tagId, "Rule match requires 'output' tag instead of 'emit'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions optimizer/src/main/java/dev/cel/optimizer/AstMutator.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -664,8 +664,8 @@ private CelMutableSource mangleIdentsInMacroSource(
return newSource;
}

private static CelMutableSource combine(
CelMutableSource celSource1, CelMutableSource celSource2) {
/** Combines two {@link CelMutableSource} instances into a single new instance. */
public static CelMutableSource combine(CelMutableSource celSource1, CelMutableSource celSource2) {
return CelMutableSource.newInstance()
.setDescription(
Strings.isNullOrEmpty(celSource1.getDescription())
Expand All@@ -677,6 +677,7 @@ private static CelMutableSource combine(
.addAllMacroCalls(celSource2.getMacroCalls());
}


/**
* Stabilizes the incoming AST by ensuring that all of expr IDs are consistently renumbered
* (monotonically increased) from the starting seed ID. If the AST contains any macro calls, its
Expand Down
4 changes: 4 additions & 0 deletions policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ java_library(
name = "compiled_rule",
srcs = ["CelCompiledRule.java"],
deps = [
":policy",
"//:auto_value",
"//bundle:cel",
"//common:cel_ast",
Expand DownExpand Up@@ -246,6 +247,7 @@ java_library(
srcs = ["RuleComposer.java"],
deps = [
":compiled_rule",
":policy",
"//bundle:cel",
"//common:cel_ast",
"//common:compiler_common",
Expand All@@ -256,11 +258,13 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
"@maven//:com_google_guava_guava",
"@maven//:org_jspecify_jspecify",
],
)
15 changes: 13 additions & 2 deletions policy/src/main/java/dev/cel/policy/CelCompiledRule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@
import dev.cel.common.ast.CelConstant;
import dev.cel.common.ast.CelExpr;
import dev.cel.common.formats.ValueString;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import java.util.Optional;

/**
Expand All@@ -43,11 +44,20 @@ public abstract class CelCompiledRule {

public abstract Cel cel();

public abstract EvaluationSemantic semantic();

/**
* HasOptionalOutput returns whether the rule returns a concrete or optional value. The rule may
* return an optional value if all match expressions under the rule are conditional.
*/
public boolean hasOptionalOutput() {
// AGGREGATE rules always return a concrete list (falling back to an empty list rather than
// optional.none()), meaning they are never optional structurally. This also prevents dead
// code evasion inside parent FIRST_MATCH rules.
if (semantic() == EvaluationSemantic.AGGREGATE) {
return false;
}

boolean isOptionalOutput = false;
for (CelCompiledMatch match : matches()) {
if (match.result().kind().equals(CelCompiledMatch.Result.Kind.RULE)
Expand DownExpand Up@@ -157,7 +167,8 @@ static CelCompiledRule create(
Optional<ValueString> ruleId,
ImmutableList<CelCompiledVariable> variables,
ImmutableList<CelCompiledMatch> matches,
Cel cel) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel);
Cel cel,
CelPolicy.EvaluationSemantic semantic) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel, semantic);
}
}
13 changes: 12 additions & 1 deletion policy/src/main/java/dev/cel/policy/CelPolicy.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@
@AutoValue
public abstract class CelPolicy {

/** Evaluation semantic for a rule. */
public enum EvaluationSemantic {
FIRST_MATCH,
AGGREGATE
}

public abstract ValueString name();

public abstract Optional<ValueString> description();
Expand DownExpand Up@@ -176,12 +182,15 @@ public abstract static class Rule {

public abstract ImmutableSet<Match> matches();

public abstract EvaluationSemantic semantic();

/** Builder for {@link Rule}. */
public static Builder newBuilder(long id) {
return new AutoValue_CelPolicy_Rule.Builder()
.setId(id)
.setVariables(ImmutableSet.of())
.setMatches(ImmutableSet.of());
.setMatches(ImmutableSet.of())
.setSemantic(EvaluationSemantic.FIRST_MATCH);
}

/** Creates a new builder to construct a {@link Rule} instance. */
Expand DownExpand Up@@ -228,6 +237,8 @@ public Builder addMatches(Iterable<Match> matches) {

abstract Rule.Builder setMatches(ImmutableSet<Match> matches);

public abstract Rule.Builder setSemantic(EvaluationSemantic semantic);

public abstract Rule build();
}
}
Expand Down
41 changes: 36 additions & 5 deletions policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result.Kind;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Match;
import dev.cel.policy.CelPolicy.Variable;
Expand DownExpand Up@@ -91,7 +92,8 @@ public CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationE
extendedCel = extendedCel.toCelBuilder().setContainer(containerBuilder.build()).build();
}

CelCompiledRule compiledRule = compileRuleImpl(policy.rule(), extendedCel, compilerContext);
CelCompiledRule compiledRule =
compileRuleImpl(policy.rule(), extendedCel, compilerContext, false);
if (compilerContext.hasError()) {
throw new CelPolicyValidationException(compilerContext.getIssueString());
}
Expand DownExpand Up@@ -172,7 +174,14 @@ private void assertAstDepthIsSafe(CelAbstractSyntaxTree ast, Cel cel)
}

private CelCompiledRule compileRuleImpl(
CelPolicy.Rule rule, Cel ruleCel, CompilerContext compilerContext) {
CelPolicy.Rule rule,
Cel ruleCel,
CompilerContext compilerContext,
boolean hasAggregateAncestor) {
if (hasAggregateAncestor && rule.semantic().equals(EvaluationSemantic.AGGREGATE)) {
compilerContext.addIssue(
rule.id(), CelIssue.formatError(1, 0, "nested aggregate rules are not allowed"));
}
// A local CEL environment used to compile a single rule. This temporary environment
// is used to declare policy variables iteratively in a given policy, ensuring proper scoping
// across a single / nested rule.
Expand DownExpand Up@@ -227,8 +236,11 @@ private CelCompiledRule compileRuleImpl(
matchResult = Result.ofOutput(output.id(), outputAst);
break;
case RULE:
boolean nextHasAggregateAncestor =
hasAggregateAncestor || rule.semantic().equals(EvaluationSemantic.AGGREGATE);
CelCompiledRule nestedRule =
compileRuleImpl(match.result().rule(), localCel, compilerContext);
compileRuleImpl(
match.result().rule(), localCel, compilerContext, nextHasAggregateAncestor);
matchResult = Result.ofRule(nestedRule);
break;
default:
Expand All@@ -240,7 +252,12 @@ private CelCompiledRule compileRuleImpl(

CelCompiledRule compiledRule =
CelCompiledRule.create(
rule.id(), rule.ruleId(), variableBuilder.build(), matchBuilder.build(), ruleCel);
rule.id(),
rule.ruleId(),
variableBuilder.build(),
matchBuilder.build(),
ruleCel,
rule.semantic());

// Validate that all branches in the policy are reachable
checkUnreachableCode(compiledRule, compilerContext);
Expand All@@ -255,6 +272,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
CelCompiledMatch compiledMatch = compiledMatches.get(i);
boolean isTriviallyTrue = compiledMatch.isConditionTriviallyTrue();

// Flag literally false conditions as dead code regardless of semantic
if (isConditionLiterallyFalse(compiledMatch.condition())) {
compilerContext.addIssue(
compiledMatch.sourceId(), CelIssue.formatError(1, 0, "Condition is always false"));
}

// If the match is a single output or a nested rule that always returns a value, it is
// exhaustive. If the condition is trivially true, then all subsequent branches are
// unreachable.
Expand All@@ -263,7 +286,9 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
&& (compiledMatch.result().kind().equals(Kind.OUTPUT)
|| !compiledMatch.result().rule().hasOptionalOutput());

if (isExhaustive && i != matchCount - 1) {
if (compiledRule.semantic() == EvaluationSemantic.FIRST_MATCH
&& isExhaustive
&& i != matchCount - 1) {
if (compiledMatch.result().kind().equals(Kind.OUTPUT)) {
compilerContext.addIssue(
compiledMatch.sourceId(),
Expand All@@ -277,6 +302,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
}
}

private static boolean isConditionLiterallyFalse(CelAbstractSyntaxTree condition) {
CelExpr celExpr = condition.getExpr();
return celExpr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)
&& !celExpr.constant().booleanValue();
}

private static CelAbstractSyntaxTree newErrorAst() {
return CelAbstractSyntaxTree.newParsedAst(
CelExpr.ofConstant(0, CelConstant.ofValue("*error*")), CelSource.newBuilder().build());
Expand Down
50 changes: 47 additions & 3 deletions policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@
import dev.cel.common.formats.YamlHelper.YamlNodeType;
import dev.cel.common.formats.YamlParserContextImpl;
import dev.cel.common.internal.CelCodePointArray;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Invariant;
import dev.cel.policy.CelPolicy.Match;
Expand DownExpand Up@@ -271,6 +272,8 @@ public CelPolicy.Rule parseRule(
return ruleBuilder.build();
}

boolean hasMatch = false;
boolean hasAggregate = false;
for (NodeTuple nodeTuple : ((MappingNode) node).getValue()) {
Node key = nodeTuple.getKeyNode();
long tagId = ctx.collectMetadata(key);
Expand All@@ -290,8 +293,24 @@ public CelPolicy.Rule parseRule(
ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value));
break;
case "match":
ruleBuilder.addMatches(parseMatches(ctx, policyBuilder, value));
if (hasAggregate) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasMatch = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, false))
.setSemantic(EvaluationSemantic.FIRST_MATCH);
break;
case "aggregate":
if (hasMatch) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasAggregate = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, true))
.setSemantic(EvaluationSemantic.AGGREGATE);
break;

default:
tagVisitor.visitRuleTag(ctx, tagId, fieldName, value, policyBuilder, ruleBuilder);
break;
Expand All@@ -301,7 +320,10 @@ public CelPolicy.Rule parseRule(
}

private ImmutableSet<CelPolicy.Match> parseMatches(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long valueId = ctx.collectMetadata(node);
ImmutableSet.Builder<CelPolicy.Match> matchesBuilder = ImmutableSet.builder();
if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
Expand All@@ -310,7 +332,7 @@ private ImmutableSet<CelPolicy.Match> parseMatches(

SequenceNode matchListNode = (SequenceNode) node;
for (Node elementNode : matchListNode.getValue()) {
matchesBuilder.add(parseMatch(ctx, policyBuilder, elementNode));
matchesBuilder.add(parseMatchInternal(ctx, policyBuilder, elementNode, isAggregate));
}

return matchesBuilder.build();
Expand All@@ -319,6 +341,14 @@ private ImmutableSet<CelPolicy.Match> parseMatches(
@Override
public CelPolicy.Match parseMatch(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
return parseMatchInternal(ctx, policyBuilder, node, false);
}

private CelPolicy.Match parseMatchInternal(
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long nodeId = ctx.collectMetadata(node);
if (!assertYamlType(ctx, nodeId, node, YamlNodeType.MAP)) {
return ERROR_MATCH;
Expand All@@ -339,6 +369,20 @@ public CelPolicy.Match parseMatch(
matchBuilder.setCondition(ctx.newSourceString(value));
break;
case "output":
if (isAggregate) {
ctx.reportError(tagId, "Rule aggregate requires 'emit' tag instead of 'output'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
.ifPresent(
result -> ctx.reportError(tagId, "Only the rule or the output may be set"));
matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value)));
break;
case "emit":
if (!isAggregate) {
ctx.reportError(tagId, "Rule match requires 'output' tag instead of 'emit'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions optimizer/src/main/java/dev/cel/optimizer/AstMutator.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -664,8 +664,8 @@ private CelMutableSource mangleIdentsInMacroSource(
return newSource;
}

private static CelMutableSource combine(
CelMutableSource celSource1, CelMutableSource celSource2) {
/** Combines two {@link CelMutableSource} instances into a single new instance. */
public static CelMutableSource combine(CelMutableSource celSource1, CelMutableSource celSource2) {
return CelMutableSource.newInstance()
.setDescription(
Strings.isNullOrEmpty(celSource1.getDescription())
Expand All@@ -677,6 +677,7 @@ private static CelMutableSource combine(
.addAllMacroCalls(celSource2.getMacroCalls());
}


/**
* Stabilizes the incoming AST by ensuring that all of expr IDs are consistently renumbered
* (monotonically increased) from the starting seed ID. If the AST contains any macro calls, its
Expand Down
4 changes: 4 additions & 0 deletions policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ java_library(
name = "compiled_rule",
srcs = ["CelCompiledRule.java"],
deps = [
":policy",
"//:auto_value",
"//bundle:cel",
"//common:cel_ast",
Expand DownExpand Up@@ -246,6 +247,7 @@ java_library(
srcs = ["RuleComposer.java"],
deps = [
":compiled_rule",
":policy",
"//bundle:cel",
"//common:cel_ast",
"//common:compiler_common",
Expand All@@ -256,11 +258,13 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
"@maven//:com_google_guava_guava",
"@maven//:org_jspecify_jspecify",
],
)
15 changes: 13 additions & 2 deletions policy/src/main/java/dev/cel/policy/CelCompiledRule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@
import dev.cel.common.ast.CelConstant;
import dev.cel.common.ast.CelExpr;
import dev.cel.common.formats.ValueString;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import java.util.Optional;

/**
Expand All@@ -43,11 +44,20 @@ public abstract class CelCompiledRule {

public abstract Cel cel();

public abstract EvaluationSemantic semantic();

/**
* HasOptionalOutput returns whether the rule returns a concrete or optional value. The rule may
* return an optional value if all match expressions under the rule are conditional.
*/
public boolean hasOptionalOutput() {
// AGGREGATE rules always return a concrete list (falling back to an empty list rather than
// optional.none()), meaning they are never optional structurally. This also prevents dead
// code evasion inside parent FIRST_MATCH rules.
if (semantic() == EvaluationSemantic.AGGREGATE) {
return false;
}

boolean isOptionalOutput = false;
for (CelCompiledMatch match : matches()) {
if (match.result().kind().equals(CelCompiledMatch.Result.Kind.RULE)
Expand DownExpand Up@@ -157,7 +167,8 @@ static CelCompiledRule create(
Optional<ValueString> ruleId,
ImmutableList<CelCompiledVariable> variables,
ImmutableList<CelCompiledMatch> matches,
Cel cel) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel);
Cel cel,
CelPolicy.EvaluationSemantic semantic) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel, semantic);
}
}
13 changes: 12 additions & 1 deletion policy/src/main/java/dev/cel/policy/CelPolicy.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@
@AutoValue
public abstract class CelPolicy {

/** Evaluation semantic for a rule. */
public enum EvaluationSemantic {
FIRST_MATCH,
AGGREGATE
}

public abstract ValueString name();

public abstract Optional<ValueString> description();
Expand DownExpand Up@@ -176,12 +182,15 @@ public abstract static class Rule {

public abstract ImmutableSet<Match> matches();

public abstract EvaluationSemantic semantic();

/** Builder for {@link Rule}. */
public static Builder newBuilder(long id) {
return new AutoValue_CelPolicy_Rule.Builder()
.setId(id)
.setVariables(ImmutableSet.of())
.setMatches(ImmutableSet.of());
.setMatches(ImmutableSet.of())
.setSemantic(EvaluationSemantic.FIRST_MATCH);
}

/** Creates a new builder to construct a {@link Rule} instance. */
Expand DownExpand Up@@ -228,6 +237,8 @@ public Builder addMatches(Iterable<Match> matches) {

abstract Rule.Builder setMatches(ImmutableSet<Match> matches);

public abstract Rule.Builder setSemantic(EvaluationSemantic semantic);

public abstract Rule build();
}
}
Expand Down
41 changes: 36 additions & 5 deletions policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result.Kind;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Match;
import dev.cel.policy.CelPolicy.Variable;
Expand DownExpand Up@@ -91,7 +92,8 @@ public CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationE
extendedCel = extendedCel.toCelBuilder().setContainer(containerBuilder.build()).build();
}

CelCompiledRule compiledRule = compileRuleImpl(policy.rule(), extendedCel, compilerContext);
CelCompiledRule compiledRule =
compileRuleImpl(policy.rule(), extendedCel, compilerContext, false);
if (compilerContext.hasError()) {
throw new CelPolicyValidationException(compilerContext.getIssueString());
}
Expand DownExpand Up@@ -172,7 +174,14 @@ private void assertAstDepthIsSafe(CelAbstractSyntaxTree ast, Cel cel)
}

private CelCompiledRule compileRuleImpl(
CelPolicy.Rule rule, Cel ruleCel, CompilerContext compilerContext) {
CelPolicy.Rule rule,
Cel ruleCel,
CompilerContext compilerContext,
boolean hasAggregateAncestor) {
if (hasAggregateAncestor && rule.semantic().equals(EvaluationSemantic.AGGREGATE)) {
compilerContext.addIssue(
rule.id(), CelIssue.formatError(1, 0, "nested aggregate rules are not allowed"));
}
// A local CEL environment used to compile a single rule. This temporary environment
// is used to declare policy variables iteratively in a given policy, ensuring proper scoping
// across a single / nested rule.
Expand DownExpand Up@@ -227,8 +236,11 @@ private CelCompiledRule compileRuleImpl(
matchResult = Result.ofOutput(output.id(), outputAst);
break;
case RULE:
boolean nextHasAggregateAncestor =
hasAggregateAncestor || rule.semantic().equals(EvaluationSemantic.AGGREGATE);
CelCompiledRule nestedRule =
compileRuleImpl(match.result().rule(), localCel, compilerContext);
compileRuleImpl(
match.result().rule(), localCel, compilerContext, nextHasAggregateAncestor);
matchResult = Result.ofRule(nestedRule);
break;
default:
Expand All@@ -240,7 +252,12 @@ private CelCompiledRule compileRuleImpl(

CelCompiledRule compiledRule =
CelCompiledRule.create(
rule.id(), rule.ruleId(), variableBuilder.build(), matchBuilder.build(), ruleCel);
rule.id(),
rule.ruleId(),
variableBuilder.build(),
matchBuilder.build(),
ruleCel,
rule.semantic());

// Validate that all branches in the policy are reachable
checkUnreachableCode(compiledRule, compilerContext);
Expand All@@ -255,6 +272,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
CelCompiledMatch compiledMatch = compiledMatches.get(i);
boolean isTriviallyTrue = compiledMatch.isConditionTriviallyTrue();

// Flag literally false conditions as dead code regardless of semantic
if (isConditionLiterallyFalse(compiledMatch.condition())) {
compilerContext.addIssue(
compiledMatch.sourceId(), CelIssue.formatError(1, 0, "Condition is always false"));
}

// If the match is a single output or a nested rule that always returns a value, it is
// exhaustive. If the condition is trivially true, then all subsequent branches are
// unreachable.
Expand All@@ -263,7 +286,9 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
&& (compiledMatch.result().kind().equals(Kind.OUTPUT)
|| !compiledMatch.result().rule().hasOptionalOutput());

if (isExhaustive && i != matchCount - 1) {
if (compiledRule.semantic() == EvaluationSemantic.FIRST_MATCH
&& isExhaustive
&& i != matchCount - 1) {
if (compiledMatch.result().kind().equals(Kind.OUTPUT)) {
compilerContext.addIssue(
compiledMatch.sourceId(),
Expand All@@ -277,6 +302,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
}
}

private static boolean isConditionLiterallyFalse(CelAbstractSyntaxTree condition) {
CelExpr celExpr = condition.getExpr();
return celExpr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)
&& !celExpr.constant().booleanValue();
}

private static CelAbstractSyntaxTree newErrorAst() {
return CelAbstractSyntaxTree.newParsedAst(
CelExpr.ofConstant(0, CelConstant.ofValue("*error*")), CelSource.newBuilder().build());
Expand Down
50 changes: 47 additions & 3 deletions policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@
import dev.cel.common.formats.YamlHelper.YamlNodeType;
import dev.cel.common.formats.YamlParserContextImpl;
import dev.cel.common.internal.CelCodePointArray;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Invariant;
import dev.cel.policy.CelPolicy.Match;
Expand DownExpand Up@@ -271,6 +272,8 @@ public CelPolicy.Rule parseRule(
return ruleBuilder.build();
}

boolean hasMatch = false;
boolean hasAggregate = false;
for (NodeTuple nodeTuple : ((MappingNode) node).getValue()) {
Node key = nodeTuple.getKeyNode();
long tagId = ctx.collectMetadata(key);
Expand All@@ -290,8 +293,24 @@ public CelPolicy.Rule parseRule(
ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value));
break;
case "match":
ruleBuilder.addMatches(parseMatches(ctx, policyBuilder, value));
if (hasAggregate) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasMatch = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, false))
.setSemantic(EvaluationSemantic.FIRST_MATCH);
break;
case "aggregate":
if (hasMatch) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasAggregate = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, true))
.setSemantic(EvaluationSemantic.AGGREGATE);
break;

default:
tagVisitor.visitRuleTag(ctx, tagId, fieldName, value, policyBuilder, ruleBuilder);
break;
Expand All@@ -301,7 +320,10 @@ public CelPolicy.Rule parseRule(
}

private ImmutableSet<CelPolicy.Match> parseMatches(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long valueId = ctx.collectMetadata(node);
ImmutableSet.Builder<CelPolicy.Match> matchesBuilder = ImmutableSet.builder();
if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
Expand All@@ -310,7 +332,7 @@ private ImmutableSet<CelPolicy.Match> parseMatches(

SequenceNode matchListNode = (SequenceNode) node;
for (Node elementNode : matchListNode.getValue()) {
matchesBuilder.add(parseMatch(ctx, policyBuilder, elementNode));
matchesBuilder.add(parseMatchInternal(ctx, policyBuilder, elementNode, isAggregate));
}

return matchesBuilder.build();
Expand All@@ -319,6 +341,14 @@ private ImmutableSet<CelPolicy.Match> parseMatches(
@Override
public CelPolicy.Match parseMatch(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
return parseMatchInternal(ctx, policyBuilder, node, false);
}

private CelPolicy.Match parseMatchInternal(
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long nodeId = ctx.collectMetadata(node);
if (!assertYamlType(ctx, nodeId, node, YamlNodeType.MAP)) {
return ERROR_MATCH;
Expand All@@ -339,6 +369,20 @@ public CelPolicy.Match parseMatch(
matchBuilder.setCondition(ctx.newSourceString(value));
break;
case "output":
if (isAggregate) {
ctx.reportError(tagId, "Rule aggregate requires 'emit' tag instead of 'output'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
.ifPresent(
result -> ctx.reportError(tagId, "Only the rule or the output may be set"));
matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value)));
break;
case "emit":
if (!isAggregate) {
ctx.reportError(tagId, "Rule match requires 'output' tag instead of 'emit'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions optimizer/src/main/java/dev/cel/optimizer/AstMutator.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -664,8 +664,8 @@ private CelMutableSource mangleIdentsInMacroSource(
return newSource;
}

private static CelMutableSource combine(
CelMutableSource celSource1, CelMutableSource celSource2) {
/** Combines two {@link CelMutableSource} instances into a single new instance. */
public static CelMutableSource combine(CelMutableSource celSource1, CelMutableSource celSource2) {
return CelMutableSource.newInstance()
.setDescription(
Strings.isNullOrEmpty(celSource1.getDescription())
Expand All@@ -677,6 +677,7 @@ private static CelMutableSource combine(
.addAllMacroCalls(celSource2.getMacroCalls());
}


/**
* Stabilizes the incoming AST by ensuring that all of expr IDs are consistently renumbered
* (monotonically increased) from the starting seed ID. If the AST contains any macro calls, its
Expand Down
4 changes: 4 additions & 0 deletions policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ java_library(
name = "compiled_rule",
srcs = ["CelCompiledRule.java"],
deps = [
":policy",
"//:auto_value",
"//bundle:cel",
"//common:cel_ast",
Expand DownExpand Up@@ -246,6 +247,7 @@ java_library(
srcs = ["RuleComposer.java"],
deps = [
":compiled_rule",
":policy",
"//bundle:cel",
"//common:cel_ast",
"//common:compiler_common",
Expand All@@ -256,11 +258,13 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
"@maven//:com_google_guava_guava",
"@maven//:org_jspecify_jspecify",
],
)
15 changes: 13 additions & 2 deletions policy/src/main/java/dev/cel/policy/CelCompiledRule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@
import dev.cel.common.ast.CelConstant;
import dev.cel.common.ast.CelExpr;
import dev.cel.common.formats.ValueString;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import java.util.Optional;

/**
Expand All@@ -43,11 +44,20 @@ public abstract class CelCompiledRule {

public abstract Cel cel();

public abstract EvaluationSemantic semantic();

/**
* HasOptionalOutput returns whether the rule returns a concrete or optional value. The rule may
* return an optional value if all match expressions under the rule are conditional.
*/
public boolean hasOptionalOutput() {
// AGGREGATE rules always return a concrete list (falling back to an empty list rather than
// optional.none()), meaning they are never optional structurally. This also prevents dead
// code evasion inside parent FIRST_MATCH rules.
if (semantic() == EvaluationSemantic.AGGREGATE) {
return false;
}

boolean isOptionalOutput = false;
for (CelCompiledMatch match : matches()) {
if (match.result().kind().equals(CelCompiledMatch.Result.Kind.RULE)
Expand DownExpand Up@@ -157,7 +167,8 @@ static CelCompiledRule create(
Optional<ValueString> ruleId,
ImmutableList<CelCompiledVariable> variables,
ImmutableList<CelCompiledMatch> matches,
Cel cel) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel);
Cel cel,
CelPolicy.EvaluationSemantic semantic) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel, semantic);
}
}
13 changes: 12 additions & 1 deletion policy/src/main/java/dev/cel/policy/CelPolicy.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@
@AutoValue
public abstract class CelPolicy {

/** Evaluation semantic for a rule. */
public enum EvaluationSemantic {
FIRST_MATCH,
AGGREGATE
}

public abstract ValueString name();

public abstract Optional<ValueString> description();
Expand DownExpand Up@@ -176,12 +182,15 @@ public abstract static class Rule {

public abstract ImmutableSet<Match> matches();

public abstract EvaluationSemantic semantic();

/** Builder for {@link Rule}. */
public static Builder newBuilder(long id) {
return new AutoValue_CelPolicy_Rule.Builder()
.setId(id)
.setVariables(ImmutableSet.of())
.setMatches(ImmutableSet.of());
.setMatches(ImmutableSet.of())
.setSemantic(EvaluationSemantic.FIRST_MATCH);
}

/** Creates a new builder to construct a {@link Rule} instance. */
Expand DownExpand Up@@ -228,6 +237,8 @@ public Builder addMatches(Iterable<Match> matches) {

abstract Rule.Builder setMatches(ImmutableSet<Match> matches);

public abstract Rule.Builder setSemantic(EvaluationSemantic semantic);

public abstract Rule build();
}
}
Expand Down
41 changes: 36 additions & 5 deletions policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result.Kind;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Match;
import dev.cel.policy.CelPolicy.Variable;
Expand DownExpand Up@@ -91,7 +92,8 @@ public CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationE
extendedCel = extendedCel.toCelBuilder().setContainer(containerBuilder.build()).build();
}

CelCompiledRule compiledRule = compileRuleImpl(policy.rule(), extendedCel, compilerContext);
CelCompiledRule compiledRule =
compileRuleImpl(policy.rule(), extendedCel, compilerContext, false);
if (compilerContext.hasError()) {
throw new CelPolicyValidationException(compilerContext.getIssueString());
}
Expand DownExpand Up@@ -172,7 +174,14 @@ private void assertAstDepthIsSafe(CelAbstractSyntaxTree ast, Cel cel)
}

private CelCompiledRule compileRuleImpl(
CelPolicy.Rule rule, Cel ruleCel, CompilerContext compilerContext) {
CelPolicy.Rule rule,
Cel ruleCel,
CompilerContext compilerContext,
boolean hasAggregateAncestor) {
if (hasAggregateAncestor && rule.semantic().equals(EvaluationSemantic.AGGREGATE)) {
compilerContext.addIssue(
rule.id(), CelIssue.formatError(1, 0, "nested aggregate rules are not allowed"));
}
// A local CEL environment used to compile a single rule. This temporary environment
// is used to declare policy variables iteratively in a given policy, ensuring proper scoping
// across a single / nested rule.
Expand DownExpand Up@@ -227,8 +236,11 @@ private CelCompiledRule compileRuleImpl(
matchResult = Result.ofOutput(output.id(), outputAst);
break;
case RULE:
boolean nextHasAggregateAncestor =
hasAggregateAncestor || rule.semantic().equals(EvaluationSemantic.AGGREGATE);
CelCompiledRule nestedRule =
compileRuleImpl(match.result().rule(), localCel, compilerContext);
compileRuleImpl(
match.result().rule(), localCel, compilerContext, nextHasAggregateAncestor);
matchResult = Result.ofRule(nestedRule);
break;
default:
Expand All@@ -240,7 +252,12 @@ private CelCompiledRule compileRuleImpl(

CelCompiledRule compiledRule =
CelCompiledRule.create(
rule.id(), rule.ruleId(), variableBuilder.build(), matchBuilder.build(), ruleCel);
rule.id(),
rule.ruleId(),
variableBuilder.build(),
matchBuilder.build(),
ruleCel,
rule.semantic());

// Validate that all branches in the policy are reachable
checkUnreachableCode(compiledRule, compilerContext);
Expand All@@ -255,6 +272,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
CelCompiledMatch compiledMatch = compiledMatches.get(i);
boolean isTriviallyTrue = compiledMatch.isConditionTriviallyTrue();

// Flag literally false conditions as dead code regardless of semantic
if (isConditionLiterallyFalse(compiledMatch.condition())) {
compilerContext.addIssue(
compiledMatch.sourceId(), CelIssue.formatError(1, 0, "Condition is always false"));
}

// If the match is a single output or a nested rule that always returns a value, it is
// exhaustive. If the condition is trivially true, then all subsequent branches are
// unreachable.
Expand All@@ -263,7 +286,9 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
&& (compiledMatch.result().kind().equals(Kind.OUTPUT)
|| !compiledMatch.result().rule().hasOptionalOutput());

if (isExhaustive && i != matchCount - 1) {
if (compiledRule.semantic() == EvaluationSemantic.FIRST_MATCH
&& isExhaustive
&& i != matchCount - 1) {
if (compiledMatch.result().kind().equals(Kind.OUTPUT)) {
compilerContext.addIssue(
compiledMatch.sourceId(),
Expand All@@ -277,6 +302,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
}
}

private static boolean isConditionLiterallyFalse(CelAbstractSyntaxTree condition) {
CelExpr celExpr = condition.getExpr();
return celExpr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)
&& !celExpr.constant().booleanValue();
}

private static CelAbstractSyntaxTree newErrorAst() {
return CelAbstractSyntaxTree.newParsedAst(
CelExpr.ofConstant(0, CelConstant.ofValue("*error*")), CelSource.newBuilder().build());
Expand Down
50 changes: 47 additions & 3 deletions policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@
import dev.cel.common.formats.YamlHelper.YamlNodeType;
import dev.cel.common.formats.YamlParserContextImpl;
import dev.cel.common.internal.CelCodePointArray;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Invariant;
import dev.cel.policy.CelPolicy.Match;
Expand DownExpand Up@@ -271,6 +272,8 @@ public CelPolicy.Rule parseRule(
return ruleBuilder.build();
}

boolean hasMatch = false;
boolean hasAggregate = false;
for (NodeTuple nodeTuple : ((MappingNode) node).getValue()) {
Node key = nodeTuple.getKeyNode();
long tagId = ctx.collectMetadata(key);
Expand All@@ -290,8 +293,24 @@ public CelPolicy.Rule parseRule(
ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value));
break;
case "match":
ruleBuilder.addMatches(parseMatches(ctx, policyBuilder, value));
if (hasAggregate) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasMatch = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, false))
.setSemantic(EvaluationSemantic.FIRST_MATCH);
break;
case "aggregate":
if (hasMatch) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasAggregate = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, true))
.setSemantic(EvaluationSemantic.AGGREGATE);
break;

default:
tagVisitor.visitRuleTag(ctx, tagId, fieldName, value, policyBuilder, ruleBuilder);
break;
Expand All@@ -301,7 +320,10 @@ public CelPolicy.Rule parseRule(
}

private ImmutableSet<CelPolicy.Match> parseMatches(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long valueId = ctx.collectMetadata(node);
ImmutableSet.Builder<CelPolicy.Match> matchesBuilder = ImmutableSet.builder();
if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
Expand All@@ -310,7 +332,7 @@ private ImmutableSet<CelPolicy.Match> parseMatches(

SequenceNode matchListNode = (SequenceNode) node;
for (Node elementNode : matchListNode.getValue()) {
matchesBuilder.add(parseMatch(ctx, policyBuilder, elementNode));
matchesBuilder.add(parseMatchInternal(ctx, policyBuilder, elementNode, isAggregate));
}

return matchesBuilder.build();
Expand All@@ -319,6 +341,14 @@ private ImmutableSet<CelPolicy.Match> parseMatches(
@Override
public CelPolicy.Match parseMatch(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
return parseMatchInternal(ctx, policyBuilder, node, false);
}

private CelPolicy.Match parseMatchInternal(
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long nodeId = ctx.collectMetadata(node);
if (!assertYamlType(ctx, nodeId, node, YamlNodeType.MAP)) {
return ERROR_MATCH;
Expand All@@ -339,6 +369,20 @@ public CelPolicy.Match parseMatch(
matchBuilder.setCondition(ctx.newSourceString(value));
break;
case "output":
if (isAggregate) {
ctx.reportError(tagId, "Rule aggregate requires 'emit' tag instead of 'output'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
.ifPresent(
result -> ctx.reportError(tagId, "Only the rule or the output may be set"));
matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value)));
break;
case "emit":
if (!isAggregate) {
ctx.reportError(tagId, "Rule match requires 'output' tag instead of 'emit'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions optimizer/src/main/java/dev/cel/optimizer/AstMutator.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -664,8 +664,8 @@ private CelMutableSource mangleIdentsInMacroSource(
return newSource;
}

private static CelMutableSource combine(
CelMutableSource celSource1, CelMutableSource celSource2) {
/** Combines two {@link CelMutableSource} instances into a single new instance. */
public static CelMutableSource combine(CelMutableSource celSource1, CelMutableSource celSource2) {
return CelMutableSource.newInstance()
.setDescription(
Strings.isNullOrEmpty(celSource1.getDescription())
Expand All@@ -677,6 +677,7 @@ private static CelMutableSource combine(
.addAllMacroCalls(celSource2.getMacroCalls());
}


/**
* Stabilizes the incoming AST by ensuring that all of expr IDs are consistently renumbered
* (monotonically increased) from the starting seed ID. If the AST contains any macro calls, its
Expand Down
4 changes: 4 additions & 0 deletions policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ java_library(
name = "compiled_rule",
srcs = ["CelCompiledRule.java"],
deps = [
":policy",
"//:auto_value",
"//bundle:cel",
"//common:cel_ast",
Expand DownExpand Up@@ -246,6 +247,7 @@ java_library(
srcs = ["RuleComposer.java"],
deps = [
":compiled_rule",
":policy",
"//bundle:cel",
"//common:cel_ast",
"//common:compiler_common",
Expand All@@ -256,11 +258,13 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
"@maven//:com_google_guava_guava",
"@maven//:org_jspecify_jspecify",
],
)
15 changes: 13 additions & 2 deletions policy/src/main/java/dev/cel/policy/CelCompiledRule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@
import dev.cel.common.ast.CelConstant;
import dev.cel.common.ast.CelExpr;
import dev.cel.common.formats.ValueString;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import java.util.Optional;

/**
Expand All@@ -43,11 +44,20 @@ public abstract class CelCompiledRule {

public abstract Cel cel();

public abstract EvaluationSemantic semantic();

/**
* HasOptionalOutput returns whether the rule returns a concrete or optional value. The rule may
* return an optional value if all match expressions under the rule are conditional.
*/
public boolean hasOptionalOutput() {
// AGGREGATE rules always return a concrete list (falling back to an empty list rather than
// optional.none()), meaning they are never optional structurally. This also prevents dead
// code evasion inside parent FIRST_MATCH rules.
if (semantic() == EvaluationSemantic.AGGREGATE) {
return false;
}

boolean isOptionalOutput = false;
for (CelCompiledMatch match : matches()) {
if (match.result().kind().equals(CelCompiledMatch.Result.Kind.RULE)
Expand DownExpand Up@@ -157,7 +167,8 @@ static CelCompiledRule create(
Optional<ValueString> ruleId,
ImmutableList<CelCompiledVariable> variables,
ImmutableList<CelCompiledMatch> matches,
Cel cel) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel);
Cel cel,
CelPolicy.EvaluationSemantic semantic) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel, semantic);
}
}
13 changes: 12 additions & 1 deletion policy/src/main/java/dev/cel/policy/CelPolicy.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@
@AutoValue
public abstract class CelPolicy {

/** Evaluation semantic for a rule. */
public enum EvaluationSemantic {
FIRST_MATCH,
AGGREGATE
}

public abstract ValueString name();

public abstract Optional<ValueString> description();
Expand DownExpand Up@@ -176,12 +182,15 @@ public abstract static class Rule {

public abstract ImmutableSet<Match> matches();

public abstract EvaluationSemantic semantic();

/** Builder for {@link Rule}. */
public static Builder newBuilder(long id) {
return new AutoValue_CelPolicy_Rule.Builder()
.setId(id)
.setVariables(ImmutableSet.of())
.setMatches(ImmutableSet.of());
.setMatches(ImmutableSet.of())
.setSemantic(EvaluationSemantic.FIRST_MATCH);
}

/** Creates a new builder to construct a {@link Rule} instance. */
Expand DownExpand Up@@ -228,6 +237,8 @@ public Builder addMatches(Iterable<Match> matches) {

abstract Rule.Builder setMatches(ImmutableSet<Match> matches);

public abstract Rule.Builder setSemantic(EvaluationSemantic semantic);

public abstract Rule build();
}
}
Expand Down
41 changes: 36 additions & 5 deletions policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result.Kind;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Match;
import dev.cel.policy.CelPolicy.Variable;
Expand DownExpand Up@@ -91,7 +92,8 @@ public CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationE
extendedCel = extendedCel.toCelBuilder().setContainer(containerBuilder.build()).build();
}

CelCompiledRule compiledRule = compileRuleImpl(policy.rule(), extendedCel, compilerContext);
CelCompiledRule compiledRule =
compileRuleImpl(policy.rule(), extendedCel, compilerContext, false);
if (compilerContext.hasError()) {
throw new CelPolicyValidationException(compilerContext.getIssueString());
}
Expand DownExpand Up@@ -172,7 +174,14 @@ private void assertAstDepthIsSafe(CelAbstractSyntaxTree ast, Cel cel)
}

private CelCompiledRule compileRuleImpl(
CelPolicy.Rule rule, Cel ruleCel, CompilerContext compilerContext) {
CelPolicy.Rule rule,
Cel ruleCel,
CompilerContext compilerContext,
boolean hasAggregateAncestor) {
if (hasAggregateAncestor && rule.semantic().equals(EvaluationSemantic.AGGREGATE)) {
compilerContext.addIssue(
rule.id(), CelIssue.formatError(1, 0, "nested aggregate rules are not allowed"));
}
// A local CEL environment used to compile a single rule. This temporary environment
// is used to declare policy variables iteratively in a given policy, ensuring proper scoping
// across a single / nested rule.
Expand DownExpand Up@@ -227,8 +236,11 @@ private CelCompiledRule compileRuleImpl(
matchResult = Result.ofOutput(output.id(), outputAst);
break;
case RULE:
boolean nextHasAggregateAncestor =
hasAggregateAncestor || rule.semantic().equals(EvaluationSemantic.AGGREGATE);
CelCompiledRule nestedRule =
compileRuleImpl(match.result().rule(), localCel, compilerContext);
compileRuleImpl(
match.result().rule(), localCel, compilerContext, nextHasAggregateAncestor);
matchResult = Result.ofRule(nestedRule);
break;
default:
Expand All@@ -240,7 +252,12 @@ private CelCompiledRule compileRuleImpl(

CelCompiledRule compiledRule =
CelCompiledRule.create(
rule.id(), rule.ruleId(), variableBuilder.build(), matchBuilder.build(), ruleCel);
rule.id(),
rule.ruleId(),
variableBuilder.build(),
matchBuilder.build(),
ruleCel,
rule.semantic());

// Validate that all branches in the policy are reachable
checkUnreachableCode(compiledRule, compilerContext);
Expand All@@ -255,6 +272,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
CelCompiledMatch compiledMatch = compiledMatches.get(i);
boolean isTriviallyTrue = compiledMatch.isConditionTriviallyTrue();

// Flag literally false conditions as dead code regardless of semantic
if (isConditionLiterallyFalse(compiledMatch.condition())) {
compilerContext.addIssue(
compiledMatch.sourceId(), CelIssue.formatError(1, 0, "Condition is always false"));
}

// If the match is a single output or a nested rule that always returns a value, it is
// exhaustive. If the condition is trivially true, then all subsequent branches are
// unreachable.
Expand All@@ -263,7 +286,9 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
&& (compiledMatch.result().kind().equals(Kind.OUTPUT)
|| !compiledMatch.result().rule().hasOptionalOutput());

if (isExhaustive && i != matchCount - 1) {
if (compiledRule.semantic() == EvaluationSemantic.FIRST_MATCH
&& isExhaustive
&& i != matchCount - 1) {
if (compiledMatch.result().kind().equals(Kind.OUTPUT)) {
compilerContext.addIssue(
compiledMatch.sourceId(),
Expand All@@ -277,6 +302,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
}
}

private static boolean isConditionLiterallyFalse(CelAbstractSyntaxTree condition) {
CelExpr celExpr = condition.getExpr();
return celExpr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)
&& !celExpr.constant().booleanValue();
}

private static CelAbstractSyntaxTree newErrorAst() {
return CelAbstractSyntaxTree.newParsedAst(
CelExpr.ofConstant(0, CelConstant.ofValue("*error*")), CelSource.newBuilder().build());
Expand Down
50 changes: 47 additions & 3 deletions policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@
import dev.cel.common.formats.YamlHelper.YamlNodeType;
import dev.cel.common.formats.YamlParserContextImpl;
import dev.cel.common.internal.CelCodePointArray;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Invariant;
import dev.cel.policy.CelPolicy.Match;
Expand DownExpand Up@@ -271,6 +272,8 @@ public CelPolicy.Rule parseRule(
return ruleBuilder.build();
}

boolean hasMatch = false;
boolean hasAggregate = false;
for (NodeTuple nodeTuple : ((MappingNode) node).getValue()) {
Node key = nodeTuple.getKeyNode();
long tagId = ctx.collectMetadata(key);
Expand All@@ -290,8 +293,24 @@ public CelPolicy.Rule parseRule(
ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value));
break;
case "match":
ruleBuilder.addMatches(parseMatches(ctx, policyBuilder, value));
if (hasAggregate) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasMatch = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, false))
.setSemantic(EvaluationSemantic.FIRST_MATCH);
break;
case "aggregate":
if (hasMatch) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasAggregate = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, true))
.setSemantic(EvaluationSemantic.AGGREGATE);
break;

default:
tagVisitor.visitRuleTag(ctx, tagId, fieldName, value, policyBuilder, ruleBuilder);
break;
Expand All@@ -301,7 +320,10 @@ public CelPolicy.Rule parseRule(
}

private ImmutableSet<CelPolicy.Match> parseMatches(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long valueId = ctx.collectMetadata(node);
ImmutableSet.Builder<CelPolicy.Match> matchesBuilder = ImmutableSet.builder();
if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
Expand All@@ -310,7 +332,7 @@ private ImmutableSet<CelPolicy.Match> parseMatches(

SequenceNode matchListNode = (SequenceNode) node;
for (Node elementNode : matchListNode.getValue()) {
matchesBuilder.add(parseMatch(ctx, policyBuilder, elementNode));
matchesBuilder.add(parseMatchInternal(ctx, policyBuilder, elementNode, isAggregate));
}

return matchesBuilder.build();
Expand All@@ -319,6 +341,14 @@ private ImmutableSet<CelPolicy.Match> parseMatches(
@Override
public CelPolicy.Match parseMatch(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
return parseMatchInternal(ctx, policyBuilder, node, false);
}

private CelPolicy.Match parseMatchInternal(
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long nodeId = ctx.collectMetadata(node);
if (!assertYamlType(ctx, nodeId, node, YamlNodeType.MAP)) {
return ERROR_MATCH;
Expand All@@ -339,6 +369,20 @@ public CelPolicy.Match parseMatch(
matchBuilder.setCondition(ctx.newSourceString(value));
break;
case "output":
if (isAggregate) {
ctx.reportError(tagId, "Rule aggregate requires 'emit' tag instead of 'output'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
.ifPresent(
result -> ctx.reportError(tagId, "Only the rule or the output may be set"));
matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value)));
break;
case "emit":
if (!isAggregate) {
ctx.reportError(tagId, "Rule match requires 'output' tag instead of 'emit'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions optimizer/src/main/java/dev/cel/optimizer/AstMutator.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -664,8 +664,8 @@ private CelMutableSource mangleIdentsInMacroSource(
return newSource;
}

private static CelMutableSource combine(
CelMutableSource celSource1, CelMutableSource celSource2) {
/** Combines two {@link CelMutableSource} instances into a single new instance. */
public static CelMutableSource combine(CelMutableSource celSource1, CelMutableSource celSource2) {
return CelMutableSource.newInstance()
.setDescription(
Strings.isNullOrEmpty(celSource1.getDescription())
Expand All@@ -677,6 +677,7 @@ private static CelMutableSource combine(
.addAllMacroCalls(celSource2.getMacroCalls());
}


/**
* Stabilizes the incoming AST by ensuring that all of expr IDs are consistently renumbered
* (monotonically increased) from the starting seed ID. If the AST contains any macro calls, its
Expand Down
4 changes: 4 additions & 0 deletions policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ java_library(
name = "compiled_rule",
srcs = ["CelCompiledRule.java"],
deps = [
":policy",
"//:auto_value",
"//bundle:cel",
"//common:cel_ast",
Expand DownExpand Up@@ -246,6 +247,7 @@ java_library(
srcs = ["RuleComposer.java"],
deps = [
":compiled_rule",
":policy",
"//bundle:cel",
"//common:cel_ast",
"//common:compiler_common",
Expand All@@ -256,11 +258,13 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
"@maven//:com_google_guava_guava",
"@maven//:org_jspecify_jspecify",
],
)
15 changes: 13 additions & 2 deletions policy/src/main/java/dev/cel/policy/CelCompiledRule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@
import dev.cel.common.ast.CelConstant;
import dev.cel.common.ast.CelExpr;
import dev.cel.common.formats.ValueString;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import java.util.Optional;

/**
Expand All@@ -43,11 +44,20 @@ public abstract class CelCompiledRule {

public abstract Cel cel();

public abstract EvaluationSemantic semantic();

/**
* HasOptionalOutput returns whether the rule returns a concrete or optional value. The rule may
* return an optional value if all match expressions under the rule are conditional.
*/
public boolean hasOptionalOutput() {
// AGGREGATE rules always return a concrete list (falling back to an empty list rather than
// optional.none()), meaning they are never optional structurally. This also prevents dead
// code evasion inside parent FIRST_MATCH rules.
if (semantic() == EvaluationSemantic.AGGREGATE) {
return false;
}

boolean isOptionalOutput = false;
for (CelCompiledMatch match : matches()) {
if (match.result().kind().equals(CelCompiledMatch.Result.Kind.RULE)
Expand DownExpand Up@@ -157,7 +167,8 @@ static CelCompiledRule create(
Optional<ValueString> ruleId,
ImmutableList<CelCompiledVariable> variables,
ImmutableList<CelCompiledMatch> matches,
Cel cel) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel);
Cel cel,
CelPolicy.EvaluationSemantic semantic) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel, semantic);
}
}
13 changes: 12 additions & 1 deletion policy/src/main/java/dev/cel/policy/CelPolicy.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@
@AutoValue
public abstract class CelPolicy {

/** Evaluation semantic for a rule. */
public enum EvaluationSemantic {
FIRST_MATCH,
AGGREGATE
}

public abstract ValueString name();

public abstract Optional<ValueString> description();
Expand DownExpand Up@@ -176,12 +182,15 @@ public abstract static class Rule {

public abstract ImmutableSet<Match> matches();

public abstract EvaluationSemantic semantic();

/** Builder for {@link Rule}. */
public static Builder newBuilder(long id) {
return new AutoValue_CelPolicy_Rule.Builder()
.setId(id)
.setVariables(ImmutableSet.of())
.setMatches(ImmutableSet.of());
.setMatches(ImmutableSet.of())
.setSemantic(EvaluationSemantic.FIRST_MATCH);
}

/** Creates a new builder to construct a {@link Rule} instance. */
Expand DownExpand Up@@ -228,6 +237,8 @@ public Builder addMatches(Iterable<Match> matches) {

abstract Rule.Builder setMatches(ImmutableSet<Match> matches);

public abstract Rule.Builder setSemantic(EvaluationSemantic semantic);

public abstract Rule build();
}
}
Expand Down
41 changes: 36 additions & 5 deletions policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result.Kind;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Match;
import dev.cel.policy.CelPolicy.Variable;
Expand DownExpand Up@@ -91,7 +92,8 @@ public CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationE
extendedCel = extendedCel.toCelBuilder().setContainer(containerBuilder.build()).build();
}

CelCompiledRule compiledRule = compileRuleImpl(policy.rule(), extendedCel, compilerContext);
CelCompiledRule compiledRule =
compileRuleImpl(policy.rule(), extendedCel, compilerContext, false);
if (compilerContext.hasError()) {
throw new CelPolicyValidationException(compilerContext.getIssueString());
}
Expand DownExpand Up@@ -172,7 +174,14 @@ private void assertAstDepthIsSafe(CelAbstractSyntaxTree ast, Cel cel)
}

private CelCompiledRule compileRuleImpl(
CelPolicy.Rule rule, Cel ruleCel, CompilerContext compilerContext) {
CelPolicy.Rule rule,
Cel ruleCel,
CompilerContext compilerContext,
boolean hasAggregateAncestor) {
if (hasAggregateAncestor && rule.semantic().equals(EvaluationSemantic.AGGREGATE)) {
compilerContext.addIssue(
rule.id(), CelIssue.formatError(1, 0, "nested aggregate rules are not allowed"));
}
// A local CEL environment used to compile a single rule. This temporary environment
// is used to declare policy variables iteratively in a given policy, ensuring proper scoping
// across a single / nested rule.
Expand DownExpand Up@@ -227,8 +236,11 @@ private CelCompiledRule compileRuleImpl(
matchResult = Result.ofOutput(output.id(), outputAst);
break;
case RULE:
boolean nextHasAggregateAncestor =
hasAggregateAncestor || rule.semantic().equals(EvaluationSemantic.AGGREGATE);
CelCompiledRule nestedRule =
compileRuleImpl(match.result().rule(), localCel, compilerContext);
compileRuleImpl(
match.result().rule(), localCel, compilerContext, nextHasAggregateAncestor);
matchResult = Result.ofRule(nestedRule);
break;
default:
Expand All@@ -240,7 +252,12 @@ private CelCompiledRule compileRuleImpl(

CelCompiledRule compiledRule =
CelCompiledRule.create(
rule.id(), rule.ruleId(), variableBuilder.build(), matchBuilder.build(), ruleCel);
rule.id(),
rule.ruleId(),
variableBuilder.build(),
matchBuilder.build(),
ruleCel,
rule.semantic());

// Validate that all branches in the policy are reachable
checkUnreachableCode(compiledRule, compilerContext);
Expand All@@ -255,6 +272,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
CelCompiledMatch compiledMatch = compiledMatches.get(i);
boolean isTriviallyTrue = compiledMatch.isConditionTriviallyTrue();

// Flag literally false conditions as dead code regardless of semantic
if (isConditionLiterallyFalse(compiledMatch.condition())) {
compilerContext.addIssue(
compiledMatch.sourceId(), CelIssue.formatError(1, 0, "Condition is always false"));
}

// If the match is a single output or a nested rule that always returns a value, it is
// exhaustive. If the condition is trivially true, then all subsequent branches are
// unreachable.
Expand All@@ -263,7 +286,9 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
&& (compiledMatch.result().kind().equals(Kind.OUTPUT)
|| !compiledMatch.result().rule().hasOptionalOutput());

if (isExhaustive && i != matchCount - 1) {
if (compiledRule.semantic() == EvaluationSemantic.FIRST_MATCH
&& isExhaustive
&& i != matchCount - 1) {
if (compiledMatch.result().kind().equals(Kind.OUTPUT)) {
compilerContext.addIssue(
compiledMatch.sourceId(),
Expand All@@ -277,6 +302,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
}
}

private static boolean isConditionLiterallyFalse(CelAbstractSyntaxTree condition) {
CelExpr celExpr = condition.getExpr();
return celExpr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)
&& !celExpr.constant().booleanValue();
}

private static CelAbstractSyntaxTree newErrorAst() {
return CelAbstractSyntaxTree.newParsedAst(
CelExpr.ofConstant(0, CelConstant.ofValue("*error*")), CelSource.newBuilder().build());
Expand Down
50 changes: 47 additions & 3 deletions policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@
import dev.cel.common.formats.YamlHelper.YamlNodeType;
import dev.cel.common.formats.YamlParserContextImpl;
import dev.cel.common.internal.CelCodePointArray;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Invariant;
import dev.cel.policy.CelPolicy.Match;
Expand DownExpand Up@@ -271,6 +272,8 @@ public CelPolicy.Rule parseRule(
return ruleBuilder.build();
}

boolean hasMatch = false;
boolean hasAggregate = false;
for (NodeTuple nodeTuple : ((MappingNode) node).getValue()) {
Node key = nodeTuple.getKeyNode();
long tagId = ctx.collectMetadata(key);
Expand All@@ -290,8 +293,24 @@ public CelPolicy.Rule parseRule(
ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value));
break;
case "match":
ruleBuilder.addMatches(parseMatches(ctx, policyBuilder, value));
if (hasAggregate) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasMatch = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, false))
.setSemantic(EvaluationSemantic.FIRST_MATCH);
break;
case "aggregate":
if (hasMatch) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasAggregate = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, true))
.setSemantic(EvaluationSemantic.AGGREGATE);
break;

default:
tagVisitor.visitRuleTag(ctx, tagId, fieldName, value, policyBuilder, ruleBuilder);
break;
Expand All@@ -301,7 +320,10 @@ public CelPolicy.Rule parseRule(
}

private ImmutableSet<CelPolicy.Match> parseMatches(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long valueId = ctx.collectMetadata(node);
ImmutableSet.Builder<CelPolicy.Match> matchesBuilder = ImmutableSet.builder();
if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
Expand All@@ -310,7 +332,7 @@ private ImmutableSet<CelPolicy.Match> parseMatches(

SequenceNode matchListNode = (SequenceNode) node;
for (Node elementNode : matchListNode.getValue()) {
matchesBuilder.add(parseMatch(ctx, policyBuilder, elementNode));
matchesBuilder.add(parseMatchInternal(ctx, policyBuilder, elementNode, isAggregate));
}

return matchesBuilder.build();
Expand All@@ -319,6 +341,14 @@ private ImmutableSet<CelPolicy.Match> parseMatches(
@Override
public CelPolicy.Match parseMatch(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
return parseMatchInternal(ctx, policyBuilder, node, false);
}

private CelPolicy.Match parseMatchInternal(
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long nodeId = ctx.collectMetadata(node);
if (!assertYamlType(ctx, nodeId, node, YamlNodeType.MAP)) {
return ERROR_MATCH;
Expand All@@ -339,6 +369,20 @@ public CelPolicy.Match parseMatch(
matchBuilder.setCondition(ctx.newSourceString(value));
break;
case "output":
if (isAggregate) {
ctx.reportError(tagId, "Rule aggregate requires 'emit' tag instead of 'output'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
.ifPresent(
result -> ctx.reportError(tagId, "Only the rule or the output may be set"));
matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value)));
break;
case "emit":
if (!isAggregate) {
ctx.reportError(tagId, "Rule match requires 'output' tag instead of 'emit'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions optimizer/src/main/java/dev/cel/optimizer/AstMutator.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -664,8 +664,8 @@ private CelMutableSource mangleIdentsInMacroSource(
return newSource;
}

private static CelMutableSource combine(
CelMutableSource celSource1, CelMutableSource celSource2) {
/** Combines two {@link CelMutableSource} instances into a single new instance. */
public static CelMutableSource combine(CelMutableSource celSource1, CelMutableSource celSource2) {
return CelMutableSource.newInstance()
.setDescription(
Strings.isNullOrEmpty(celSource1.getDescription())
Expand All@@ -677,6 +677,7 @@ private static CelMutableSource combine(
.addAllMacroCalls(celSource2.getMacroCalls());
}


/**
* Stabilizes the incoming AST by ensuring that all of expr IDs are consistently renumbered
* (monotonically increased) from the starting seed ID. If the AST contains any macro calls, its
Expand Down
4 changes: 4 additions & 0 deletions policy/src/main/java/dev/cel/policy/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,7 @@ java_library(
name = "compiled_rule",
srcs = ["CelCompiledRule.java"],
deps = [
":policy",
"//:auto_value",
"//bundle:cel",
"//common:cel_ast",
Expand DownExpand Up@@ -246,6 +247,7 @@ java_library(
srcs = ["RuleComposer.java"],
deps = [
":compiled_rule",
":policy",
"//bundle:cel",
"//common:cel_ast",
"//common:compiler_common",
Expand All@@ -256,11 +258,13 @@ java_library(
"//common/ast:mutable_expr",
"//common/formats:value_string",
"//common/navigation:mutable_navigation",
"//common/types",
"//common/types:cel_types",
"//common/types:type_providers",
"//extensions:optional_library",
"//optimizer:ast_optimizer",
"//optimizer:mutable_ast",
"@maven//:com_google_guava_guava",
"@maven//:org_jspecify_jspecify",
],
)
15 changes: 13 additions & 2 deletions policy/src/main/java/dev/cel/policy/CelCompiledRule.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@
import dev.cel.common.ast.CelConstant;
import dev.cel.common.ast.CelExpr;
import dev.cel.common.formats.ValueString;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import java.util.Optional;

/**
Expand All@@ -43,11 +44,20 @@ public abstract class CelCompiledRule {

public abstract Cel cel();

public abstract EvaluationSemantic semantic();

/**
* HasOptionalOutput returns whether the rule returns a concrete or optional value. The rule may
* return an optional value if all match expressions under the rule are conditional.
*/
public boolean hasOptionalOutput() {
// AGGREGATE rules always return a concrete list (falling back to an empty list rather than
// optional.none()), meaning they are never optional structurally. This also prevents dead
// code evasion inside parent FIRST_MATCH rules.
if (semantic() == EvaluationSemantic.AGGREGATE) {
return false;
}

boolean isOptionalOutput = false;
for (CelCompiledMatch match : matches()) {
if (match.result().kind().equals(CelCompiledMatch.Result.Kind.RULE)
Expand DownExpand Up@@ -157,7 +167,8 @@ static CelCompiledRule create(
Optional<ValueString> ruleId,
ImmutableList<CelCompiledVariable> variables,
ImmutableList<CelCompiledMatch> matches,
Cel cel) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel);
Cel cel,
CelPolicy.EvaluationSemantic semantic) {
return new AutoValue_CelCompiledRule(sourceId, ruleId, variables, matches, cel, semantic);
}
}
13 changes: 12 additions & 1 deletion policy/src/main/java/dev/cel/policy/CelPolicy.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,12 @@
@AutoValue
public abstract class CelPolicy {

/** Evaluation semantic for a rule. */
public enum EvaluationSemantic {
FIRST_MATCH,
AGGREGATE
}

public abstract ValueString name();

public abstract Optional<ValueString> description();
Expand DownExpand Up@@ -176,12 +182,15 @@ public abstract static class Rule {

public abstract ImmutableSet<Match> matches();

public abstract EvaluationSemantic semantic();

/** Builder for {@link Rule}. */
public static Builder newBuilder(long id) {
return new AutoValue_CelPolicy_Rule.Builder()
.setId(id)
.setVariables(ImmutableSet.of())
.setMatches(ImmutableSet.of());
.setMatches(ImmutableSet.of())
.setSemantic(EvaluationSemantic.FIRST_MATCH);
}

/** Creates a new builder to construct a {@link Rule} instance. */
Expand DownExpand Up@@ -228,6 +237,8 @@ public Builder addMatches(Iterable<Match> matches) {

abstract Rule.Builder setMatches(ImmutableSet<Match> matches);

public abstract Rule.Builder setSemantic(EvaluationSemantic semantic);

public abstract Rule build();
}
}
Expand Down
41 changes: 36 additions & 5 deletions policy/src/main/java/dev/cel/policy/CelPolicyCompilerImpl.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,7 @@
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result;
import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result.Kind;
import dev.cel.policy.CelCompiledRule.CelCompiledVariable;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Match;
import dev.cel.policy.CelPolicy.Variable;
Expand DownExpand Up@@ -91,7 +92,8 @@ public CelCompiledRule compileRule(CelPolicy policy) throws CelPolicyValidationE
extendedCel = extendedCel.toCelBuilder().setContainer(containerBuilder.build()).build();
}

CelCompiledRule compiledRule = compileRuleImpl(policy.rule(), extendedCel, compilerContext);
CelCompiledRule compiledRule =
compileRuleImpl(policy.rule(), extendedCel, compilerContext, false);
if (compilerContext.hasError()) {
throw new CelPolicyValidationException(compilerContext.getIssueString());
}
Expand DownExpand Up@@ -172,7 +174,14 @@ private void assertAstDepthIsSafe(CelAbstractSyntaxTree ast, Cel cel)
}

private CelCompiledRule compileRuleImpl(
CelPolicy.Rule rule, Cel ruleCel, CompilerContext compilerContext) {
CelPolicy.Rule rule,
Cel ruleCel,
CompilerContext compilerContext,
boolean hasAggregateAncestor) {
if (hasAggregateAncestor && rule.semantic().equals(EvaluationSemantic.AGGREGATE)) {
compilerContext.addIssue(
rule.id(), CelIssue.formatError(1, 0, "nested aggregate rules are not allowed"));
}
// A local CEL environment used to compile a single rule. This temporary environment
// is used to declare policy variables iteratively in a given policy, ensuring proper scoping
// across a single / nested rule.
Expand DownExpand Up@@ -227,8 +236,11 @@ private CelCompiledRule compileRuleImpl(
matchResult = Result.ofOutput(output.id(), outputAst);
break;
case RULE:
boolean nextHasAggregateAncestor =
hasAggregateAncestor || rule.semantic().equals(EvaluationSemantic.AGGREGATE);
CelCompiledRule nestedRule =
compileRuleImpl(match.result().rule(), localCel, compilerContext);
compileRuleImpl(
match.result().rule(), localCel, compilerContext, nextHasAggregateAncestor);
matchResult = Result.ofRule(nestedRule);
break;
default:
Expand All@@ -240,7 +252,12 @@ private CelCompiledRule compileRuleImpl(

CelCompiledRule compiledRule =
CelCompiledRule.create(
rule.id(), rule.ruleId(), variableBuilder.build(), matchBuilder.build(), ruleCel);
rule.id(),
rule.ruleId(),
variableBuilder.build(),
matchBuilder.build(),
ruleCel,
rule.semantic());

// Validate that all branches in the policy are reachable
checkUnreachableCode(compiledRule, compilerContext);
Expand All@@ -255,6 +272,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
CelCompiledMatch compiledMatch = compiledMatches.get(i);
boolean isTriviallyTrue = compiledMatch.isConditionTriviallyTrue();

// Flag literally false conditions as dead code regardless of semantic
if (isConditionLiterallyFalse(compiledMatch.condition())) {
compilerContext.addIssue(
compiledMatch.sourceId(), CelIssue.formatError(1, 0, "Condition is always false"));
}

// If the match is a single output or a nested rule that always returns a value, it is
// exhaustive. If the condition is trivially true, then all subsequent branches are
// unreachable.
Expand All@@ -263,7 +286,9 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
&& (compiledMatch.result().kind().equals(Kind.OUTPUT)
|| !compiledMatch.result().rule().hasOptionalOutput());

if (isExhaustive && i != matchCount - 1) {
if (compiledRule.semantic() == EvaluationSemantic.FIRST_MATCH
&& isExhaustive
&& i != matchCount - 1) {
if (compiledMatch.result().kind().equals(Kind.OUTPUT)) {
compilerContext.addIssue(
compiledMatch.sourceId(),
Expand All@@ -277,6 +302,12 @@ private void checkUnreachableCode(CelCompiledRule compiledRule, CompilerContext
}
}

private static boolean isConditionLiterallyFalse(CelAbstractSyntaxTree condition) {
CelExpr celExpr = condition.getExpr();
return celExpr.constantOrDefault().getKind().equals(CelConstant.Kind.BOOLEAN_VALUE)
&& !celExpr.constant().booleanValue();
}

private static CelAbstractSyntaxTree newErrorAst() {
return CelAbstractSyntaxTree.newParsedAst(
CelExpr.ofConstant(0, CelConstant.ofValue("*error*")), CelSource.newBuilder().build());
Expand Down
50 changes: 47 additions & 3 deletions policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@
import dev.cel.common.formats.YamlHelper.YamlNodeType;
import dev.cel.common.formats.YamlParserContextImpl;
import dev.cel.common.internal.CelCodePointArray;
import dev.cel.policy.CelPolicy.EvaluationSemantic;
import dev.cel.policy.CelPolicy.Import;
import dev.cel.policy.CelPolicy.Invariant;
import dev.cel.policy.CelPolicy.Match;
Expand DownExpand Up@@ -271,6 +272,8 @@ public CelPolicy.Rule parseRule(
return ruleBuilder.build();
}

boolean hasMatch = false;
boolean hasAggregate = false;
for (NodeTuple nodeTuple : ((MappingNode) node).getValue()) {
Node key = nodeTuple.getKeyNode();
long tagId = ctx.collectMetadata(key);
Expand All@@ -290,8 +293,24 @@ public CelPolicy.Rule parseRule(
ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value));
break;
case "match":
ruleBuilder.addMatches(parseMatches(ctx, policyBuilder, value));
if (hasAggregate) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasMatch = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, false))
.setSemantic(EvaluationSemantic.FIRST_MATCH);
break;
case "aggregate":
if (hasMatch) {
ctx.reportError(tagId, "Only one of 'match' or 'aggregate' may be set in a rule");
}
hasAggregate = true;
ruleBuilder
.addMatches(parseMatches(ctx, policyBuilder, value, true))
.setSemantic(EvaluationSemantic.AGGREGATE);
break;

default:
tagVisitor.visitRuleTag(ctx, tagId, fieldName, value, policyBuilder, ruleBuilder);
break;
Expand All@@ -301,7 +320,10 @@ public CelPolicy.Rule parseRule(
}

private ImmutableSet<CelPolicy.Match> parseMatches(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long valueId = ctx.collectMetadata(node);
ImmutableSet.Builder<CelPolicy.Match> matchesBuilder = ImmutableSet.builder();
if (!assertYamlType(ctx, valueId, node, YamlNodeType.LIST)) {
Expand All@@ -310,7 +332,7 @@ private ImmutableSet<CelPolicy.Match> parseMatches(

SequenceNode matchListNode = (SequenceNode) node;
for (Node elementNode : matchListNode.getValue()) {
matchesBuilder.add(parseMatch(ctx, policyBuilder, elementNode));
matchesBuilder.add(parseMatchInternal(ctx, policyBuilder, elementNode, isAggregate));
}

return matchesBuilder.build();
Expand All@@ -319,6 +341,14 @@ private ImmutableSet<CelPolicy.Match> parseMatches(
@Override
public CelPolicy.Match parseMatch(
PolicyParserContext<Node> ctx, CelPolicy.Builder policyBuilder, Node node) {
return parseMatchInternal(ctx, policyBuilder, node, false);
}

private CelPolicy.Match parseMatchInternal(
PolicyParserContext<Node> ctx,
CelPolicy.Builder policyBuilder,
Node node,
boolean isAggregate) {
long nodeId = ctx.collectMetadata(node);
if (!assertYamlType(ctx, nodeId, node, YamlNodeType.MAP)) {
return ERROR_MATCH;
Expand All@@ -339,6 +369,20 @@ public CelPolicy.Match parseMatch(
matchBuilder.setCondition(ctx.newSourceString(value));
break;
case "output":
if (isAggregate) {
ctx.reportError(tagId, "Rule aggregate requires 'emit' tag instead of 'output'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
.ifPresent(
result -> ctx.reportError(tagId, "Only the rule or the output may be set"));
matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value)));
break;
case "emit":
if (!isAggregate) {
ctx.reportError(tagId, "Rule match requires 'output' tag instead of 'emit'");
}
matchBuilder
.result()
.filter(result -> result.kind().equals(Match.Result.Kind.RULE))
Expand Down
Loading
Loading