diff --git a/src/builtins/strings.rs b/src/builtins/strings.rs index fd3a2ee8b..7ca949df3 100644 --- a/src/builtins/strings.rs +++ b/src/builtins/strings.rs @@ -542,15 +542,15 @@ fn strings_count( ensure_args_count(span, name, params, args, 2)?; let search = ensure_string(name, ¶ms[0], &args[0])?; - let substring = ensure_string(name, ¶ms[0], &args[1])?; - - Ok(Value::from( - search - .as_bytes() - .windows(substring.len()) - .filter(|&w| w == substring.as_bytes()) - .count(), - )) + let substring = ensure_string(name, ¶ms[1], &args[1])?; + + if substring.is_empty() { + // An empty needle matches between every character (and at both ends), + // consistent with Go's strings.Count and OPA semantics. + return Ok(Value::from(search.chars().count().saturating_add(1))); + } + + Ok(Value::from(search.matches(substring.as_ref()).count())) } fn startswith(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { diff --git a/tests/interpreter/cases/builtins/strings/strings_count.yaml b/tests/interpreter/cases/builtins/strings/strings_count.yaml new file mode 100644 index 000000000..5de134629 --- /dev/null +++ b/tests/interpreter/cases/builtins/strings/strings_count.yaml @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: basic match + data: {} + modules: [] + query: 'x := strings.count("cheese", "e")' + want_result: + x: 3 + + - note: no match + data: {} + modules: [] + query: 'x := strings.count("dummy", "x")' + want_result: + x: 0 + + - note: multiple separate matches + data: {} + modules: [] + query: 'x := strings.count("hello hello hello world", "hello")' + want_result: + x: 3 + + - note: empty needle returns char_count+1 + data: {} + modules: [] + query: 'x := strings.count("abc", "")' + want_result: + x: 4 + + - note: empty needle on empty string + data: {} + modules: [] + query: 'x := strings.count("", "")' + want_result: + x: 1 + + - note: empty needle on single char + data: {} + modules: [] + query: 'x := strings.count("a", "")' + want_result: + x: 2 + + - note: non-overlapping matches only + data: {} + modules: [] + query: 'x := strings.count("aaaa", "aa")' + want_result: + x: 2 + + - note: multibyte utf8 string with empty needle + data: {} + modules: [] + query: 'x := strings.count("\u4e16\u754c", "")' + want_result: + x: 3