Parser ensures that pipe expressions have a % arg (#1933)
Closes https://github.com/KittyCAD/modeling-app/issues/1411
This commit is contained in:
		@ -986,6 +986,17 @@ impl CallExpression {
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Is at least one argument the '%' i.e. the substitution operator?
 | 
			
		||||
    pub fn has_substitution_arg(&self) -> bool {
 | 
			
		||||
        self.arguments
 | 
			
		||||
            .iter()
 | 
			
		||||
            .any(|arg| matches!(arg, Value::PipeSubstitution(_)))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn as_source_ranges(&self) -> Vec<SourceRange> {
 | 
			
		||||
        vec![SourceRange([self.start, self.end])]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn replace_value(&mut self, source_range: SourceRange, new_value: Value) {
 | 
			
		||||
        for arg in &mut self.arguments {
 | 
			
		||||
            arg.replace_value(source_range, new_value.clone());
 | 
			
		||||
 | 
			
		||||
@ -139,7 +139,7 @@ fn pipe_expression(i: TokenSlice) -> PResult<PipeExpression> {
 | 
			
		||||
    let mut values = vec![head];
 | 
			
		||||
    let value_surrounded_by_comments = (
 | 
			
		||||
        repeat(0.., preceded(opt(whitespace), non_code_node)), // Before the value
 | 
			
		||||
        preceded(opt(whitespace), value_allowed_in_pipe_expr), // The value
 | 
			
		||||
        preceded(opt(whitespace), fn_call),                    // The value
 | 
			
		||||
        repeat(0.., noncode_just_after_code),                  // After the value
 | 
			
		||||
    );
 | 
			
		||||
    let tail: Vec<(Vec<_>, _, Vec<_>)> = repeat(
 | 
			
		||||
@ -151,7 +151,23 @@ fn pipe_expression(i: TokenSlice) -> PResult<PipeExpression> {
 | 
			
		||||
    ))
 | 
			
		||||
    .parse_next(i)?;
 | 
			
		||||
 | 
			
		||||
    // All child parsers have been run. Time to structure the return value.
 | 
			
		||||
    // All child parsers have been run.
 | 
			
		||||
    // First, ensure they all have a % in their args.
 | 
			
		||||
    let calls_without_substitution = tail.iter().find_map(|(_nc, call_expr, _nc2)| {
 | 
			
		||||
        if !call_expr.has_substitution_arg() {
 | 
			
		||||
            Some(call_expr.as_source_ranges())
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    if let Some(source_ranges) = calls_without_substitution {
 | 
			
		||||
        let err = KclError::Syntax(KclErrorDetails {
 | 
			
		||||
            source_ranges,
 | 
			
		||||
            message: "All expressions in a pipeline must use the % (substitution operator)".to_owned(),
 | 
			
		||||
        });
 | 
			
		||||
        return Err(ErrMode::Cut(err.into()));
 | 
			
		||||
    }
 | 
			
		||||
    // Time to structure the return value.
 | 
			
		||||
    let mut code_count = 0;
 | 
			
		||||
    let mut max_noncode_end = 0;
 | 
			
		||||
    for (noncode_before, code, noncode_after) in tail {
 | 
			
		||||
@ -159,7 +175,7 @@ fn pipe_expression(i: TokenSlice) -> PResult<PipeExpression> {
 | 
			
		||||
            max_noncode_end = nc.end.max(max_noncode_end);
 | 
			
		||||
            non_code_meta.insert(code_count, nc);
 | 
			
		||||
        }
 | 
			
		||||
        values.push(code);
 | 
			
		||||
        values.push(Value::CallExpression(Box::new(code)));
 | 
			
		||||
        code_count += 1;
 | 
			
		||||
        for nc in noncode_after {
 | 
			
		||||
            max_noncode_end = nc.end.max(max_noncode_end);
 | 
			
		||||
@ -1561,7 +1577,7 @@ const mySk1 = startSketchAt([0, 0])"#;
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn inline_comment_pipe_expression() {
 | 
			
		||||
        let test_input = r#"a('XY')
 | 
			
		||||
        |> b()
 | 
			
		||||
        |> b(%)
 | 
			
		||||
        |> c(%) // inline-comment
 | 
			
		||||
        |> d(%)"#;
 | 
			
		||||
 | 
			
		||||
@ -1778,10 +1794,10 @@ const mySk1 = startSketchAt([0, 0])"#;
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn some_pipe_expr() {
 | 
			
		||||
        let test_program = r#"x()
 | 
			
		||||
        |> y() /* this is
 | 
			
		||||
        |> y(%) /* this is
 | 
			
		||||
        a comment
 | 
			
		||||
        spanning a few lines */
 | 
			
		||||
        |> z()"#;
 | 
			
		||||
        |> z(%)"#;
 | 
			
		||||
        let tokens = crate::token::lexer(test_program);
 | 
			
		||||
        let actual = pipe_expression.parse(&tokens).unwrap();
 | 
			
		||||
        let n = actual.non_code_meta.non_code_nodes.len();
 | 
			
		||||
@ -1795,17 +1811,17 @@ const mySk1 = startSketchAt([0, 0])"#;
 | 
			
		||||
    fn comments_in_pipe_expr() {
 | 
			
		||||
        for (i, test_program) in [
 | 
			
		||||
            r#"y() |> /*hi*/ z(%)"#,
 | 
			
		||||
            "1 |>/*hi*/  f",
 | 
			
		||||
            "1 |>/*hi*/ f(%)",
 | 
			
		||||
            r#"y() |> /*hi*/ z(%)"#,
 | 
			
		||||
            "1 /*hi*/ |> f",
 | 
			
		||||
            "1 /*hi*/ |> f(%)",
 | 
			
		||||
            "1
 | 
			
		||||
        // Hi
 | 
			
		||||
        |> f",
 | 
			
		||||
        |> f(%)",
 | 
			
		||||
            "1
 | 
			
		||||
        /* Hi 
 | 
			
		||||
        there
 | 
			
		||||
        */
 | 
			
		||||
        |> f",
 | 
			
		||||
        |> f(%)",
 | 
			
		||||
        ]
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .enumerate()
 | 
			
		||||
@ -2814,6 +2830,17 @@ let myBox = box([0,0], -3, -16, -10)
 | 
			
		||||
        let parser = crate::parser::Parser::new(tokens);
 | 
			
		||||
        parser.ast().unwrap();
 | 
			
		||||
    }
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn must_use_percent_in_pipeline_fn() {
 | 
			
		||||
        let some_program_string = r#"
 | 
			
		||||
        foo()
 | 
			
		||||
            |> bar(2)
 | 
			
		||||
        "#;
 | 
			
		||||
        let tokens = crate::token::lexer(some_program_string);
 | 
			
		||||
        let parser = crate::parser::Parser::new(tokens);
 | 
			
		||||
        let err = parser.ast().unwrap_err();
 | 
			
		||||
        println!("{err}")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
@ -2864,7 +2891,7 @@ mod snapshot_tests {
 | 
			
		||||
                let tokens = crate::token::lexer($test_kcl_program);
 | 
			
		||||
                let actual = match program.parse(&tokens) {
 | 
			
		||||
                    Ok(x) => x,
 | 
			
		||||
                    Err(_e) => panic!("could not parse test"),
 | 
			
		||||
                    Err(e) => panic!("could not parse test: {e:?}"),
 | 
			
		||||
                };
 | 
			
		||||
                insta::assert_json_snapshot!(actual);
 | 
			
		||||
            }
 | 
			
		||||
@ -2972,7 +2999,7 @@ mod snapshot_tests {
 | 
			
		||||
        "const mySketch = startSketchAt([0,0]) |> lineTo([1, 1], %) |> close(%)"
 | 
			
		||||
    );
 | 
			
		||||
    snapshot_test!(ah, "const myBox = startSketchAt(p)");
 | 
			
		||||
    snapshot_test!(ai, r#"const myBox = f(1) |> g(2)"#);
 | 
			
		||||
    snapshot_test!(ai, r#"const myBox = f(1) |> g(2, %)"#);
 | 
			
		||||
    snapshot_test!(aj, r#"const myBox = startSketchAt(p) |> line([0, l], %)"#);
 | 
			
		||||
    snapshot_test!(ak, "lineTo({ to: [0, 1] })");
 | 
			
		||||
    snapshot_test!(al, "lineTo({ to: [0, 1], from: [3, 3] })");
 | 
			
		||||
 | 
			
		||||
@ -4,18 +4,18 @@ expression: actual
 | 
			
		||||
---
 | 
			
		||||
{
 | 
			
		||||
  "start": 0,
 | 
			
		||||
  "end": 26,
 | 
			
		||||
  "end": 29,
 | 
			
		||||
  "body": [
 | 
			
		||||
    {
 | 
			
		||||
      "type": "VariableDeclaration",
 | 
			
		||||
      "type": "VariableDeclaration",
 | 
			
		||||
      "start": 0,
 | 
			
		||||
      "end": 26,
 | 
			
		||||
      "end": 29,
 | 
			
		||||
      "declarations": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "VariableDeclarator",
 | 
			
		||||
          "start": 6,
 | 
			
		||||
          "end": 26,
 | 
			
		||||
          "end": 29,
 | 
			
		||||
          "id": {
 | 
			
		||||
            "type": "Identifier",
 | 
			
		||||
            "start": 6,
 | 
			
		||||
@ -26,7 +26,7 @@ expression: actual
 | 
			
		||||
            "type": "PipeExpression",
 | 
			
		||||
            "type": "PipeExpression",
 | 
			
		||||
            "start": 14,
 | 
			
		||||
            "end": 26,
 | 
			
		||||
            "end": 29,
 | 
			
		||||
            "body": [
 | 
			
		||||
              {
 | 
			
		||||
                "type": "CallExpression",
 | 
			
		||||
@ -55,7 +55,7 @@ expression: actual
 | 
			
		||||
                "type": "CallExpression",
 | 
			
		||||
                "type": "CallExpression",
 | 
			
		||||
                "start": 22,
 | 
			
		||||
                "end": 26,
 | 
			
		||||
                "end": 29,
 | 
			
		||||
                "callee": {
 | 
			
		||||
                  "type": "Identifier",
 | 
			
		||||
                  "start": 22,
 | 
			
		||||
@ -70,6 +70,12 @@ expression: actual
 | 
			
		||||
                    "end": 25,
 | 
			
		||||
                    "value": 2,
 | 
			
		||||
                    "raw": "2"
 | 
			
		||||
                  },
 | 
			
		||||
                  {
 | 
			
		||||
                    "type": "PipeSubstitution",
 | 
			
		||||
                    "type": "PipeSubstitution",
 | 
			
		||||
                    "start": 27,
 | 
			
		||||
                    "end": 28
 | 
			
		||||
                  }
 | 
			
		||||
                ],
 | 
			
		||||
                "optional": false
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user