Repository: clarkmcc/cel-rust (language: rust)

Issue:
Ability to check which functions/variables a script references
It would be nice if the parser took note of which functions and variables a given script actually made use of and made that information available to the embedding program.

My primary motivation for wanting this is so that I can cache some variables to the scripts that reference them, and only run scripts when certain variables change. Currently, there is no way to tell which scripts actually reference specific functions/variables, so there's no way to make any kind of mapping between references and which scripts reference them. Besides this, it could be used for general sandboxing notifications, like if certain functions/variables were only valid in certain contexts, the user of this library could print a coherent diagnostic about the offending reference, instead of reporting that a reference simply doesn't exist. And, even besides that, it would just be nice for debugging and diagnostics.


Discussion / hints:

Fix patch:
diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs
index 64108d2..77ffe6d 100644
--- a/interpreter/src/lib.rs
+++ b/interpreter/src/lib.rs
@@ -1,6 +1,6 @@
 extern crate core;
 
-use cel_parser::parse;
+use cel_parser::{parse, ExpressionReferences};
 use std::convert::TryFrom;
 use std::sync::Arc;
 use thiserror::Error;
@@ -126,6 +126,21 @@ impl Program {
     pub fn execute(&self, context: &Context) -> ResolveResult {
         Value::resolve(&self.expression, context)
     }
+
+    /// Returns the variables and functions referenced by the CEL program
+    ///
+    /// # Example
+    /// ```rust
+    /// # use cel_interpreter::Program;
+    /// let program = Program::compile("size(foo) > 0").unwrap();
+    /// let references = program.references();
+    ///
+    /// assert!(references.has_function("size"));
+    /// assert!(references.has_variable("foo"));
+    /// ```
+    pub fn references(&self) -> ExpressionReferences {
+        self.expression.references()
+    }
 }
 
 impl TryFrom<&str> for Program {
diff --git a/parser/src/ast.rs b/parser/src/ast.rs
index 5967ccc..6eeb54f 100644
--- a/parser/src/ast.rs
+++ b/parser/src/ast.rs
@@ -1,3 +1,4 @@
+use std::collections::HashSet;
 use std::sync::Arc;
 
 #[derive(Debug, Eq, PartialEq, Clone)]
@@ -65,3 +66,158 @@ pub enum Atom {
     Bool(bool),
     Null,
 }
+
+/// A collection of all the references that an expression makes to variables and functions.
+pub struct ExpressionReferences<'expr> {
+    variables: HashSet<&'expr str>,
+    functions: HashSet<&'expr str>,
+}
+
+impl<'expr> ExpressionReferences<'expr> {
+    /// Returns true if the expression references the provided variable name.
+    ///
+    /// # Example
+    /// ```rust
+    /// # use cel_parser::parse;
+    /// let expression = parse("foo.bar == true").unwrap();
+    /// let references = expression.references();
+    /// assert!(references.has_variable("foo"));
+    /// ```
+    pub fn has_variable(&self, name: impl AsRef<str>) -> bool {
+        self.variables.contains(name.as_ref())
+    }
+
+    /// Returns true if the expression references the provided variable name.
+    ///
+    /// # Example
+    /// ```rust
+    /// # use cel_parser::parse;
+    /// let expression = parse("size(foo) > 0").unwrap();
+    /// let references = expression.references();
+    /// assert!(references.has_function("size"));
+    /// ```
+    pub fn has_function(&self, name: impl AsRef<str>) -> bool {
+        self.functions.contains(name.as_ref())
+    }
+
+    /// Returns a list of all variables referenced in the expression.
+    ///
+    /// # Example
+    /// ```rust
+    /// # use cel_parser::parse;
+    /// let expression = parse("foo.bar == true").unwrap();
+    /// let references = expression.references();
+    /// assert_eq!(vec!["foo"], references.variables());
+    /// ```
+    pub fn variables(&self) -> Vec<&str> {
+        self.variables.iter().copied().collect()
+    }
+
+    /// Returns a list of all functions referenced in the expression.
+    ///
+    /// # Example
+    /// ```rust
+    /// # use cel_parser::parse;
+    /// let expression = parse("size(foo) > 0").unwrap();
+    /// let references = expression.references();
+    /// assert_eq!(vec!["size"], references.functions());
+    /// ```
+    pub fn functions(&self) -> Vec<&str> {
+        self.functions.iter().copied().collect()
+    }
+}
+
+impl Expression {
+    /// Returns a set of all variables referenced in the expression. Variable identifiers
+    /// are represented internally as [`Arc<String>`] and this function simply clones those
+    /// references into a [`HashSet`].
+    ///
+    /// # Example
+    /// ```rust
+    /// # use cel_parser::parse;
+    /// let expression = parse("foo && size(foo) > 0").unwrap();
+    /// let references = expression.references();
+    ///
+    /// assert!(references.has_variable("foo"));
+    /// assert!(references.has_function("size"));
+    /// ```
+    pub fn references(&self) -> ExpressionReferences {
+        let mut variables = HashSet::new();
+        let mut functions = HashSet::new();
+        self._references(&mut variables, &mut functions);
+        ExpressionReferences {
+            variables,
+            functions,
+        }
+    }
+
+    /// Internal recursive function to collect all variable and function references in the expression.
+    fn _references<'expr>(
+        &'expr self,
+        variables: &mut HashSet<&'expr str>,
+        functions: &mut HashSet<&'expr str>,
+    ) {
+        match self {
+            Expression::Arithmetic(e1, _, e2)
+            | Expression::Relation(e1, _, e2)
+            | Expression::Ternary(e1, _, e2)
+            | Expression::Or(e1, e2)
+            | Expression::And(e1, e2) => {
+                e1._references(variables, functions);
+                e2._references(variables, functions);
+            }
+            Expression::Unary(_, e) => {
+                e._references(variables, functions);
+            }
+            Expression::Member(e, _) => {
+                e._references(variables, functions);
+            }
+            Expression::FunctionCall(name, target, args) => {
+                if let Expression::Ident(v) = &**name {
+                    functions.insert(v.as_str());
+                }
+                if let Some(target) = target {
+                    target._references(variables, functions);
+                }
+                for e in args {
+                    e._references(variables, functions);
+                }
+            }
+            Expression::List(e) => {
+                for e in e {
+                    e._references(variables, functions);
+                }
+            }
+            Expression::Map(v) => {
+                for (e1, e2) in v {
+                    e1._references(variables, functions);
+                    e2._references(variables, functions);
+                }
+            }
+            Expression::Atom(_) => {}
+            Expression::Ident(v) => {
+                variables.insert(v.as_str());
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::parse;
+
+    #[test]
+    fn test_references() {
+        let expr =
+            parse("foo.bar.baz == true && size(foo) > 0 && foo[0] == 1 && bar.startsWith('a')")
+                .unwrap();
+        let refs = expr.references();
+        assert!(refs.has_variable("foo"));
+        assert!(refs.has_variable("bar"));
+        assert_eq!(refs.variables.len(), 2);
+
+        assert!(refs.has_function("size"));
+        assert!(refs.has_function("startsWith"));
+        assert_eq!(refs.functions.len(), 2);
+    }
+}
diff --git a/parser/src/lib.rs b/parser/src/lib.rs
index 0e60ff8..9f7218d 100644
--- a/parser/src/lib.rs
+++ b/parser/src/lib.rs
@@ -505,4 +505,9 @@ mod tests {
             ),
         );
     }
+
+    #[test]
+    fn test_foobar() {
+        println!("{:?}", parse("foo.bar.baz == 10 && size(requests) == 3"))
+    }
 }


Test patch: