Skip to content

Commit

Permalink
Merge branch 'web-infra-dev:main' into fix-no-loss-of-precision
Browse files Browse the repository at this point in the history
  • Loading branch information
Devin-Yeung authored Jul 30, 2023
2 parents 214eee5 + 3cf08a2 commit c2fc31b
Show file tree
Hide file tree
Showing 7 changed files with 544 additions and 122 deletions.
270 changes: 205 additions & 65 deletions crates/oxc_linter/src/jest_ast_util.rs
Original file line number Diff line number Diff line change
@@ -1,51 +1,63 @@
use std::borrow::Cow;

use oxc_ast::ast::{CallExpression, Expression, IdentifierReference};
use oxc_span::Atom;
use oxc_ast::{
ast::{CallExpression, Expression, IdentifierName, IdentifierReference, MemberExpression},
AstKind,
};
use oxc_semantic::AstNode;
use oxc_span::{Atom, Span};

use crate::context::LintContext;

pub enum JestFnKind {
Hook,
Describe,
Test,
Expect,
Jest,
Unknown,
}
pub fn parse_general_jest_fn_call<'a>(
call_expr: &'a CallExpression<'a>,
node: &AstNode<'a>,
ctx: &LintContext,
) -> Option<ParsedGeneralJestFnCall<'a>> {
let jest_fn_call = parse_jest_fn_call(call_expr, node, ctx)?;

impl JestFnKind {
pub fn from(name: &str) -> Self {
match name {
"expect" => Self::Expect,
"jest" => Self::Jest,
"describe" | "fdescribe" | "xdescribe" => Self::Describe,
"fit" | "it" | "test" | "xit" | "xtest" => Self::Test,
"beforeAll" | "beforeEach" | "afterAll" | "afterEach" => Self::Hook,
_ => Self::Unknown,
}
if let ParsedJestFnCall::GeneralJestFnCall(jest_fn_call) = jest_fn_call {
return Some(jest_fn_call);
}
}

pub struct ParsedJestFnCall<'a> {
pub kind: JestFnKind,
pub members: Vec<Cow<'a, str>>,
pub raw: Cow<'a, str>,
None
}

pub fn parse_jest_fn_call<'a>(
call_expr: &'a CallExpression,
ctx: &'a LintContext,
call_expr: &'a CallExpression<'a>,
node: &AstNode<'a>,
ctx: &LintContext,
) -> Option<ParsedJestFnCall<'a>> {
let callee = &call_expr.callee;

// if bailed out, we're not a jest function
// If bailed out, we're not jest function
let resolved = resolve_to_jest_fn(call_expr, ctx)?;

let chain = get_node_chain(callee);
// only the top level Call expression callee's parent is None, it's not necessary to set it to None, but
// I didn't know how to pass Expression to it.
let chain = get_node_chain(callee, None);
let all_member_expr_except_last = chain
.iter()
.rev()
.skip(1)
.all(|member| matches!(member.parent, Some(Expression::MemberExpression(_))));

// Check every link in the chain except the last is a member expression
if !all_member_expr_except_last {
return None;
}

// Ensure that we're at the "top" of the function call chain otherwise when
// parsing e.g. x().y.z(), we'll incorrectly find & parse "x()" even though
// the full chain is not a valid jest function call chain
if ctx.nodes().parent_node(node.id()).is_some_and(|parent_node| {
matches!(parent_node.kind(), AstKind::CallExpression(_) | AstKind::MemberExpression(_))
}) {
return None;
}

if let (Some(first), Some(last)) = (chain.first(), chain.last()) {
// if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`)
if last == "each"
// If we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`)
if last.is_name_equal("each")
&& !matches!(
callee,
Expression::CallExpression(_) | Expression::TaggedTemplateExpression(_)
Expand All @@ -54,14 +66,14 @@ pub fn parse_jest_fn_call<'a>(
return None;
}

if matches!(callee, Expression::TaggedTemplateExpression(_)) && last != "each" {
if matches!(callee, Expression::TaggedTemplateExpression(_)) && last.is_name_unequal("each")
{
return None;
}

let kind = JestFnKind::from(first);
let Some(first_name )= first.name() else { return None };
let kind = JestFnKind::from(&first_name);
let mut members = Vec::new();
let mut iter = chain.into_iter();
let first = iter.next().expect("first ident name");
let iter = chain.into_iter().skip(1);
let rest = iter;

// every member node must have a member expression as their parent
Expand All @@ -76,17 +88,19 @@ pub fn parse_jest_fn_call<'a>(
} else if members.len() == 1 {
VALID_JEST_FN_CALL_CHAINS_2
.iter()
.any(|chain| chain[0] == name && chain[1] == members[0])
.any(|chain| chain[0] == name && members[0].is_name_equal(chain[1]))
} else if members.len() == 2 {
VALID_JEST_FN_CALL_CHAINS_3
.iter()
.any(|chain| chain[0] == name && chain[1] == members[0] && chain[2] == members[1])
VALID_JEST_FN_CALL_CHAINS_3.iter().any(|chain| {
chain[0] == name
&& members[0].is_name_equal(chain[1])
&& members[1].is_name_equal(chain[2])
})
} else if members.len() == 3 {
VALID_JEST_FN_CALL_CHAINS_4.iter().any(|chain| {
chain[0] == name
&& chain[1] == members[0]
&& chain[2] == members[1]
&& chain[3] == members[2]
&& members[0].is_name_equal(chain[1])
&& members[1].is_name_equal(chain[2])
&& members[2].is_name_equal(chain[3])
})
} else {
false
Expand All @@ -95,22 +109,21 @@ pub fn parse_jest_fn_call<'a>(
if !is_valid_jest_call {
return None;
}
return Some(ParsedJestFnCall { kind, members, raw: first });
return Some(ParsedJestFnCall::GeneralJestFnCall(ParsedGeneralJestFnCall {
kind,
members,
raw: first_name,
}));
}

None
}

struct ResolvedJestFn<'a> {
pub local: &'a Atom,
}

fn resolve_to_jest_fn<'a>(
call_expr: &'a CallExpression,
ctx: &'a LintContext,
) -> Option<ResolvedJestFn<'a>> {
let ident = resolve_first_ident(&call_expr.callee)?;

if ctx.semantic().is_reference_to_global_variable(ident) {
return Some(ResolvedJestFn { local: &ident.name });
}
Expand All @@ -128,38 +141,165 @@ fn resolve_first_ident<'a>(expr: &'a Expression) -> Option<&'a IdentifierReferen
}
}

/// a.b.c -> ["a", "b"]
/// a[`b`] - > ["a", "b"]
/// a["b"] - > ["a", "b"]
/// a[b] - > ["a", "b"]
fn get_node_chain<'a>(expr: &'a Expression) -> Vec<Cow<'a, str>> {
#[derive(Clone, Copy)]
pub enum JestFnKind {
Expect,
General(JestGeneralFnKind),
Unknown,
}

impl JestFnKind {
pub fn from(name: &str) -> Self {
match name {
"expect" => Self::Expect,
"jest" => Self::General(JestGeneralFnKind::Jest),
"describe" | "fdescribe" | "xdescribe" => Self::General(JestGeneralFnKind::Describe),
"fit" | "it" | "test" | "xit" | "xtest" => Self::General(JestGeneralFnKind::Test),
"beforeAll" | "beforeEach" | "afterAll" | "afterEach" => {
Self::General(JestGeneralFnKind::Hook)
}
_ => Self::Unknown,
}
}

pub fn to_general(self) -> Option<JestGeneralFnKind> {
match self {
Self::General(kind) => Some(kind),
_ => None,
}
}
}

#[derive(Clone, Copy)]
pub enum JestGeneralFnKind {
Hook,
Describe,
Test,
Jest,
}

pub enum ParsedJestFnCall<'a> {
GeneralJestFnCall(ParsedGeneralJestFnCall<'a>),
#[allow(unused)]
ExpectFnCall(ParsedExpectFnCall<'a>),
}

pub struct ParsedGeneralJestFnCall<'a> {
pub kind: JestFnKind,
pub members: Vec<KnownMemberExpressionProperty<'a>>,
pub raw: Cow<'a, str>,
}

pub struct ParsedExpectFnCall<'a> {
pub kind: JestFnKind,
pub members: Vec<KnownMemberExpressionProperty<'a>>,
pub raw: Cow<'a, str>,
// pub args: Vec<&'a Expression<'a>>
// TODO: add `modifiers`, `matcher` for this struct.
}

struct ResolvedJestFn<'a> {
pub local: &'a Atom,
}

pub struct KnownMemberExpressionProperty<'a> {
pub element: MemberExpressionElement<'a>,
pub parent: Option<&'a Expression<'a>>,
pub span: Span,
}

impl<'a> KnownMemberExpressionProperty<'a> {
pub fn name(&self) -> Option<Cow<'a, str>> {
match &self.element {
MemberExpressionElement::Expression(expr) => match expr {
Expression::Identifier(ident) => Some(Cow::Borrowed(ident.name.as_str())),
Expression::StringLiteral(string_literal) => {
Some(Cow::Borrowed(string_literal.value.as_str()))
}
Expression::TemplateLiteral(template_literal) => Some(Cow::Borrowed(
template_literal.quasi().expect("get string content").as_str(),
)),
_ => None,
},
MemberExpressionElement::IdentName(ident_name) => {
Some(Cow::Borrowed(ident_name.name.as_str()))
}
}
}
pub fn is_name_equal(&self, name: &str) -> bool {
self.name().map_or(false, |n| n == name)
}
pub fn is_name_unequal(&self, name: &str) -> bool {
!self.is_name_equal(name)
}
}

pub enum MemberExpressionElement<'a> {
Expression(&'a Expression<'a>),
IdentName(&'a IdentifierName),
}

impl<'a> MemberExpressionElement<'a> {
pub fn from_member_expr(
member_expr: &'a MemberExpression<'a>,
) -> Option<(Span, MemberExpressionElement<'a>)> {
let Some((span, _)) = member_expr.static_property_info() else { return None };
match member_expr {
MemberExpression::ComputedMemberExpression(expr) => {
Some((span, Self::Expression(&expr.expression)))
}
MemberExpression::StaticMemberExpression(expr) => {
Some((span, Self::IdentName(&expr.property)))
}
// Jest fn chains don't have private fields, just ignore it.
MemberExpression::PrivateFieldExpression(_) => None,
}
}
}

/// Port from [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest/blob/a058f22f94774eeea7980ea2d1f24c6808bf3e2c/src/rules/utils/parseJestFnCall.ts#L36-L51)
fn get_node_chain<'a>(
expr: &'a Expression<'a>,
parent: Option<&'a Expression<'a>>,
) -> Vec<KnownMemberExpressionProperty<'a>> {
let mut chain = Vec::new();

match expr {
Expression::MemberExpression(member_expr) => {
chain.extend(get_node_chain(member_expr.object()));
if let Some(name) = member_expr.static_property_name() {
chain.push(Cow::Borrowed(name));
chain.extend(get_node_chain(member_expr.object(), Some(expr)));
if let Some((span, element)) = MemberExpressionElement::from_member_expr(member_expr) {
chain.push(KnownMemberExpressionProperty { element, parent: Some(expr), span });
}
}
Expression::Identifier(ident) => {
chain.push(Cow::Borrowed(ident.name.as_str()));
chain.push(KnownMemberExpressionProperty {
element: MemberExpressionElement::Expression(expr),
parent,
span: ident.span,
});
}
Expression::CallExpression(call_expr) => {
let sub_chain = get_node_chain(&call_expr.callee);
let sub_chain = get_node_chain(&call_expr.callee, Some(expr));
chain.extend(sub_chain);
}
Expression::TaggedTemplateExpression(tagged_expr) => {
let sub_chain = get_node_chain(&tagged_expr.tag);
let sub_chain = get_node_chain(&tagged_expr.tag, Some(expr));
chain.extend(sub_chain);
}
Expression::StringLiteral(string_literal) => {
chain.push(Cow::Borrowed(string_literal.value.as_str()));
chain.push(KnownMemberExpressionProperty {
element: MemberExpressionElement::Expression(expr),
parent,
span: string_literal.span,
});
}
Expression::TemplateLiteral(template_literal) => {
if template_literal.expressions.is_empty() && template_literal.quasis.len() == 1 {
chain.push(Cow::Borrowed(
template_literal.quasi().expect("get string content").as_str(),
));
chain.push(KnownMemberExpressionProperty {
element: MemberExpressionElement::Expression(expr),
parent,
span: template_literal.span,
});
}
}
_ => {}
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ oxc_macros::declare_all_lint_rules! {
typescript::no_var_requires,
jest::no_disabled_tests,
jest::no_test_prefixes,
jest::no_focused_tests,
}

#[cfg(test)]
Expand Down
Loading

0 comments on commit c2fc31b

Please sign in to comment.