Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: correct optional type handling in check_placeholder #345

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 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
68 changes: 48 additions & 20 deletions wdl-analysis/src/types/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -657,31 +657,32 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> {
/// Checks a placeholder expression.
pub(crate) fn check_placeholder<N: TreeNode>(&mut self, placeholder: &Placeholder<N>) {
self.placeholders += 1;

// Evaluate the placeholder expression and check that the resulting type is
// coercible to string for interpolation
let expr = placeholder.expr();
if let Some(ty) = self.evaluate_expr(&expr) {
match ty {
Type::Primitive(..) | Type::Union | Type::None => {
// OK
}
ty => {
// Check for a sep option is specified; if so, accept `Array[P]` where `P` is
// primitive.
let mut coercible = false;
if let Some(PlaceholderOption::Sep(_)) = placeholder.option() {
if let Type::Compound(CompoundType::Array(ty), _) = &ty {
if !ty.element_type().is_optional()
&& ty.element_type().as_primitive().is_some()
{
// OK
coercible = true;
}
}
if let Some(option) = placeholder.option() {
let valid = match option {
PlaceholderOption::Sep(_) => matches!(ty,
Type::Compound(CompoundType::Array(ref element_type), false)
if matches!(element_type.element_type(), Type::Primitive(_, false) | Type::Union)),
PlaceholderOption::Default(_) => matches!(ty, Type::Primitive(_, true)),
PlaceholderOption::TrueFalse(_) => {
matches!(ty, Type::Primitive(PrimitiveType::Boolean, _))
}

if !coercible {
};
if !valid {
self.context
.add_diagnostic(Self::invalid_placeholder_option(
&ty,
expr.span(),
Some(option.map(|n| n.into())),
));
}
} else {
match ty {
Type::Primitive(..) | Type::Union | Type::None => {}
_ => {
self.context
.add_diagnostic(cannot_coerce_to_string(&ty, expr.span()));
}
Expand All @@ -692,6 +693,33 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> {
self.placeholders -= 1;
}

fn invalid_placeholder_option(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move this function into diagnostics.rs, where we consolidate all diagnostic messages from static analysis.

ty: &Type,
_span: Span,
option: Option<PlaceholderOption>,
) -> Diagnostic {
let message = match option {
Some(PlaceholderOption::Sep(_)) => format!(
"type mismatch for placeholder option `sep`: expected type `Array[P]` where P: any primitive type or Union, but found `{}`",
ty
),
Some(PlaceholderOption::Default(_)) => format!(
"type mismatch for placeholder option `default`: expected an optional primitive type, but found `{}`",
ty
),
Some(PlaceholderOption::TrueFalse(_)) => format!(
"type mismatch for placeholder option `truefalse`: expected type `Boolean`, but found `{}`",
ty
),
None => format!("cannot coerce type `{}` to `String`", ty),
};
Diagnostic::error(message) // No span here
}

fn cannot_coerce_to_string(ty: &Type, _span: Span) -> Diagnostic {
Diagnostic::error(format!("cannot coerce type `{}` to `String`", ty)) // No span here
}

/// Evaluates the type of a literal array expression.
fn evaluate_literal_array<N: TreeNode>(&mut self, expr: &LiteralArray<N>) -> Type {
// Look at the first array element to determine the element type
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
error: type mismatch for placeholder option `sep`: expected type `Array[P]` where P: any primitive type or Union, but found `Boolean`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to regenerate the test baselines with BLESS=1 with the above changes to invalid_placeholder_option.

┌─ tests/analysis/placeholder-options/source.wdl:13:1
13 │

12 changes: 12 additions & 0 deletions wdl-analysis/tests/analysis/placeholder-options/source.wdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version 1.2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we can add test cases for the default and true/false options to this file as well?


task valid_case {
input {
Array[String] arr
}
command <<< ~{sep=", " arr} >>>
}

task invalid_case {
command <<< ~{sep=", " true} >>>
}
86 changes: 86 additions & 0 deletions wdl-analysis/tests/analysis/placeholders/source.diagnostics
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
error: type mismatch for placeholder option `default`: expected an optional primitive type, but found `Int`
┌─ tests/analysis/placeholders/source.wdl:13:2
13 │ }

error: type mismatch for placeholder option `sep`: expected type `Array[P]` where P: any primitive type or Union, but found `Int`
┌─ tests/analysis/placeholders/source.wdl:13:2
13 │ }

error: type mismatch for placeholder option `truefalse`: expected type `Boolean`, but found `Int`
┌─ tests/analysis/placeholders/source.wdl:13:2
13 │ }

warning[UnusedDeclaration]: unused declaration `s1`
┌─ tests/analysis/placeholders/source.wdl:5:12
5 │ String s1 = "~{true}" # OK: Boolean is coercible to string, no option
│ ^^

warning[UnusedDeclaration]: unused declaration `s2`
┌─ tests/analysis/placeholders/source.wdl:6:12
6 │ String s2 = "~{true="yes" false="no" false}" # OK: truefalse option with Boolean
│ ^^

warning[UnusedDeclaration]: unused declaration `s3`
┌─ tests/analysis/placeholders/source.wdl:7:12
7 │ String s3 = "~{true="yes" false="no" 1}" # NOT OK: truefalse expects Boolean, but got Int
│ ^^

warning[UnusedDeclaration]: unused declaration `s4`
┌─ tests/analysis/placeholders/source.wdl:8:12
8 │ String s4 = "~{sep=',' [1, 2, 3]}" # OK: sep option with Array[Int]
│ ^^

warning[UnusedDeclaration]: unused declaration `s5`
┌─ tests/analysis/placeholders/source.wdl:9:12
9 │ String s5 = "~{sep=',' 123}" # NOT OK: sep expects Array, but got Int
│ ^^

warning[UnusedDeclaration]: unused declaration `s6`
┌─ tests/analysis/placeholders/source.wdl:10:12
10 │ String s6 = "~{default="fallback" var?}" # OK: default option with optional variable
│ ^^

error: unknown name `var`
┌─ tests/analysis/placeholders/source.wdl:10:39
10 │ String s6 = "~{default="fallback" var?}" # OK: default option with optional variable
│ ^^^

error: expected `}`, but found `?`
┌─ tests/analysis/placeholders/source.wdl:10:42
10 │ String s6 = "~{default="fallback" var?}" # OK: default option with optional variable
│ -- ^ unexpected `?`
│ │
│ this placeholder start is not matched

warning[UnusedDeclaration]: unused declaration `s7`
┌─ tests/analysis/placeholders/source.wdl:11:12
11 │ String s7 = "~{default="fallback" 123}" # NOT OK: default expects optional, but got Int
│ ^^

warning[UnusedDeclaration]: unused declaration `s8`
┌─ tests/analysis/placeholders/source.wdl:12:12
12 │ String s8 = "~{[1, 2, 3]}" # NOT OK: Array without sep option, not coercible to string
│ ^^

error: cannot coerce type `Array[Int]+` to `String`
┌─ tests/analysis/placeholders/source.wdl:12:20
12 │ String s8 = "~{[1, 2, 3]}" # NOT OK: Array without sep option, not coercible to string
│ ^^^^^^^^^ this is type `Array[Int]+`

13 changes: 13 additions & 0 deletions wdl-analysis/tests/analysis/placeholders/source.wdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This is a test for analysis of placeholders and their options.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should merge this with the placeholder-options test.

version 1.1

workflow test {
String s1 = "~{true}" # OK: Boolean is coercible to string, no option
String s2 = "~{true="yes" false="no" false}" # OK: truefalse option with Boolean
String s3 = "~{true="yes" false="no" 1}" # NOT OK: truefalse expects Boolean, but got Int
String s4 = "~{sep=',' [1, 2, 3]}" # OK: sep option with Array[Int]
String s5 = "~{sep=',' 123}" # NOT OK: sep expects Array, but got Int
String s6 = "~{default="fallback" var?}" # OK: default option with optional variable
String s7 = "~{default="fallback" 123}" # NOT OK: default expects optional, but got Int
String s8 = "~{[1, 2, 3]}" # NOT OK: Array without sep option, not coercible to string
}
46 changes: 46 additions & 0 deletions wdl-analysis/tests/analysis/type_checker_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#[cfg(test)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can probably just convert these tests to corresponding file-based tests and delete this file.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-commenting here as I think we can delete this file.

mod tests {
use super::*;

#[test]
fn test_sep_with_array() {
let expr_type = Type::Array(Box::new(Type::Int)); // Array[Int]
let placeholder = Placeholder::Sep(",".to_string());
assert!(check_placeholder(&expr_type, &placeholder).is_ok());
}

#[test]
fn test_sep_with_non_array() {
let expr_type = Type::Int; // Not an array
let placeholder = Placeholder::Sep(",".to_string());
assert!(check_placeholder(&expr_type, &placeholder).is_err());
}

#[test]
fn test_default_with_optional() {
let expr_type = Type::Optional(Box::new(Type::String)); // Optional[String]
let placeholder = Placeholder::Default("default_value".to_string());
assert!(check_placeholder(&expr_type, &placeholder).is_ok());
}

#[test]
fn test_default_with_non_optional() {
let expr_type = Type::String; // Not optional
let placeholder = Placeholder::Default("default_value".to_string());
assert!(check_placeholder(&expr_type, &placeholder).is_err());
}

#[test]
fn test_true_false_with_boolean() {
let expr_type = Type::Boolean;
let placeholder = Placeholder::TrueFalse("yes".to_string(), "no".to_string());
assert!(check_placeholder(&expr_type, &placeholder).is_ok());
}

#[test]
fn test_true_false_with_non_boolean() {
let expr_type = Type::String; // Not a boolean
let placeholder = Placeholder::TrueFalse("yes".to_string(), "no".to_string());
assert!(check_placeholder(&expr_type, &placeholder).is_err());
}
}
9 changes: 9 additions & 0 deletions wdl-engine/tests/tasks/invalid-placeholder-sep/error.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
error: call to function `read_json` failed: failed to open file `attempts/0/work/not-array.json`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error should be different than this as this one is because I gave you a wrong example earlier.

I would expect a runtime error that matches exactly what would happen on analysis if the code had been {sep=',' true}.

However, there haven't been any changes to the expression evaluator in wdl-engine to do the type checks, which would be required to get the correct output here.


Caused by:
No such file or directory (os error 2)
┌─ tests/tasks/invalid-placeholder-sep/source.wdl:6:31
6 │ String a = "~{sep=',' read_json('not-array.json')}"
│ ^^^^^^^^^

1 change: 1 addition & 0 deletions wdl-engine/tests/tasks/invalid-placeholder-sep/inputs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
true
8 changes: 8 additions & 0 deletions wdl-engine/tests/tasks/invalid-placeholder-sep/source.wdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version 1.2

task test {
command<<<>>>
output {
String a = "~{sep=',' read_json('not-array.json')}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, I gave you a bad example from previous comments.

I would expect this test to look like:

version 1.2

task test {
    input {
        File f
    }
    
    command <<<>>>
    
    output {
        String out = "~{sep=',' read_json(f)}"
    }
}

With inputs.json containing:

{
    "test.f": "not-array.json"
}

}
}
6 changes: 6 additions & 0 deletions wdl-engine/tests/tasks/primitive-literals/error.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
error: failed to evaluate output `x` for task `write_file_task`: file `attempts/0/work/testdir/hello.txt` does not exist
┌─ tests/tasks/primitive-literals/source.wdl:14:10
14 │ File x = "testdir/hello.txt"
│ ^

9 changes: 9 additions & 0 deletions wdl-engine/tests/tasks/relative-and-absolute-task/error.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
error: call to function `read_string` failed: failed to read file `attempts/0/work/my/path/to/something.txt`

Caused by:
Permission denied (os error 13)
┌─ tests/tasks/relative-and-absolute-task/source.wdl:10:24
10 │ String something = read_string("my/path/to/something.txt")
│ ^^^^^^^^^^^

9 changes: 9 additions & 0 deletions wdl-engine/tests/tasks/test-join-paths/error.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
error: call to function `read_string` failed: failed to read file `attempts/0/work/mydir/mydata.txt`

Caused by:
Permission denied (os error 13)
┌─ tests/tasks/test-join-paths/source.wdl:31:21
31 │ String result = read_string(data)
│ ^^^^^^^^^^^

6 changes: 6 additions & 0 deletions wdl-engine/tests/workflows/primitive-literals/error.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
error: call to task `write_file_task` in `tests/workflows/primitive-literals/source.wdl` failed: failed to evaluate output `x` for task `write_file_task`: file `calls/write_file_task/attempts/0/work/testdir/hello.txt` does not exist
┌─ tests/workflows/primitive-literals/source.wdl:16:8
16 │ call write_file_task
│ ^^^^^^^^^^^^^^^

Loading