vue_oxc_toolkit 0.3.1

A parser to generate semantically correct AST from Vue SFCs for code linting purposes.
Documentation
use oxc_allocator::TakeIn;
use oxc_ast::{
  AstBuilder, NONE,
  ast::{
    Argument, Expression, FormalParameters, JSXChild, JSXElement, JSXExpression,
    ParenthesizedExpression, Statement,
  },
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_span::{SPAN, Span};
use regex::Regex;
use vue_compiler_core::parser::Directive;

use crate::parser::{ParserImpl, parse::SourceLocatonSpan};

pub struct VForWrapper<'a, 'b> {
  ast: &'a AstBuilder<'b>,
  data_origin: Option<ParenthesizedExpression<'b>>,
  params: Option<FormalParameters<'b>>,
}

impl<'a> ParserImpl<'a> {
  fn invalid_v_for_expression(&mut self, span: Span) -> Option<()> {
    self.errors.push(OxcDiagnostic::error("Invalid v-for expression").with_label(span));
    None
  }

  pub fn analyze_v_for(
    &mut self,
    dir: &Directive<'a>,
    wrapper: &mut VForWrapper<'_, 'a>,
  ) -> Option<()> {
    if dir.has_empty_expr() {
      self.invalid_v_for_expression(dir.location.span())?;
    }
    let expr = dir.expression.as_ref().unwrap(); // SAFETY: Checked above

    // https://github.com/vuejs/core/blob/e1ccd9fde8f57fe7bd40fdf1345692ab3e6a1fa0/packages/compiler-core/src/utils.ts#L571
    let for_alias_regex = Regex::new(r"^([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)").unwrap();
    if let Some(caps) = for_alias_regex.captures(expr.content.raw)
      && let Some(cap1) = caps.get(1)
      && let Some(cap2) = caps.get(2)
    {
      wrapper.set_data_origin(self.ast.parenthesized_expression(
        SPAN,
        self.parse_expression(cap2.as_str(), expr.location.start.offset + cap2.start() + 1)?,
      ));

      let params = cap1.as_str();
      let (str, start, should_dummy_span) =
        if params.trim().starts_with('(') && params.trim().ends_with(')') {
          (format!("{params} => 0"), expr.location.start.offset + cap1.start() + 1, false)
        } else {
          (format!("({params}) => 0"), expr.location.start.offset + cap1.start(), true)
        };

      let mut expr = self.parse_expression(self.ast.atom(&str).as_str(), start)?;

      let Expression::ArrowFunctionExpression(expression) = &mut expr else {
        // unreachable!();
        return None;
      };

      let mut params = expression.params.take_in(self.ast.allocator);
      if should_dummy_span {
        params.span = SPAN;
      }

      wrapper.set_params(params);
    } else {
      self.invalid_v_for_expression(dir.location.span())?;
    }

    Some(())
  }
}

/// Wrap the JSX element with a function call, similar to jsx {items.map(items => <div key={item.id} />)} but with vue semantic.
impl<'a, 'b> VForWrapper<'a, 'b> {
  pub const fn new(ast: &'a AstBuilder<'b>) -> Self {
    Self { ast, data_origin: None, params: None }
  }

  pub fn wrap(self, element: JSXElement<'b>) -> JSXChild<'b> {
    if self.include_v_for() {
      let Self { ast, data_origin, params } = self;
      let data_origin = data_origin.unwrap();
      let params = params.unwrap();

      ast.jsx_child_expression_container(
        SPAN,
        JSXExpression::CallExpression(ast.alloc_call_expression(
          SPAN,
          Expression::ParenthesizedExpression(ast.alloc(data_origin)),
          NONE,
          self.ast.vec1(Argument::ArrowFunctionExpression(ast.alloc_arrow_function_expression(
            SPAN,
            true,
            false,
            NONE,
            params,
            NONE,
            ast.function_body(
              SPAN,
              ast.vec(),
              ast.vec1(Statement::ExpressionStatement(ast.alloc_expression_statement(
                SPAN,
                Expression::ParenthesizedExpression(ast.alloc_parenthesized_expression(
                  SPAN,
                  Expression::JSXElement(self.ast.alloc(element)),
                )),
              ))),
            ),
          ))),
          false,
        )),
      )
    } else {
      JSXChild::Element(self.ast.alloc(element))
    }
  }
}

impl<'b> VForWrapper<'_, 'b> {
  const fn include_v_for(&self) -> bool {
    self.data_origin.is_some() && self.params.is_some()
  }

  const fn set_data_origin(&mut self, data_origin: ParenthesizedExpression<'b>) {
    self.data_origin = Some(data_origin);
  }

  const fn set_params(&mut self, params: FormalParameters<'b>) {
    self.params = Some(params);
  }
}

#[cfg(test)]
mod tests {
  use crate::test_ast;

  #[test]
  fn v_for() {
    test_ast!("directive/v-for.vue");
  }

  #[test]
  fn v_for_error() {
    test_ast!("directive/v-for-error.vue", true, true);
  }
}