Skip to content

Commit

Permalink
feat(linter): add eslint/no-spaced-func (#9360)
Browse files Browse the repository at this point in the history
  • Loading branch information
therewillbecode authored Feb 27, 2025
1 parent 23480e6 commit d38e6de
Show file tree
Hide file tree
Showing 3 changed files with 346 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ mod eslint {
pub mod no_self_compare;
pub mod no_setter_return;
pub mod no_shadow_restricted_names;
pub mod no_spaced_func;
pub mod no_sparse_arrays;
pub mod no_template_curly_in_string;
pub mod no_ternary;
Expand Down Expand Up @@ -567,6 +568,7 @@ oxc_macros::declare_all_lint_rules! {
eslint::max_lines,
eslint::max_params,
eslint::new_cap,
eslint::no_spaced_func,
eslint::no_useless_call,
eslint::no_unneeded_ternary,
eslint::no_extra_label,
Expand Down
235 changes: 235 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_spaced_func.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
use memchr::memchr;
use oxc_ast::AstKind;
use oxc_ast::ast::Expression;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};

use crate::{AstNode, context::LintContext, rule::Rule};

fn no_spaced_func_diagnostic(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn(
"Spacing between function identifiers and their applications is disallowed.",
)
.with_label(span)
}

#[derive(Debug, Default, Clone)]
pub struct NoSpacedFunc;

declare_oxc_lint!(
/// ### What it does
///
/// This rule disallows spacing between function identifiers and their applications.
///
/// ### Why is this bad?
///
/// While it’s possible to have whitespace between the name of a function and the parentheses
/// that execute it, such patterns tend to look more like errors.
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```javascript
/// fn ()
/// ```
///
/// ```javascript
/// fn
/// ()
/// ```
///
/// ```javascript
/// f.b ()
/// ```
///
/// Examples of **correct** code for this rule:
/// ```js
/// fn()
/// ```
///
/// ```javascript
/// f.b()
/// ```
NoSpacedFunc,
eslint,
style,
fix
);

#[derive(PartialEq, Debug)]
enum FuncSpace {
NotSpaced,
Spaced(Span),
}

/// Given an identifier reference will determine
/// if there is spacing in the span from the end of the
/// identifier name to the next `(` char.
///
/// For example `foo ()` would return `FuncSpace::Spaced(span)`
/// where span refers to the whitespace between `foo` and `()`.
fn get_substring_to_lparens(ctx: &LintContext, search_span: Span) -> FuncSpace {
let src = ctx.source_range(search_span);

let Some(char_count) = memchr(b'(', src.as_bytes()) else {
return FuncSpace::NotSpaced;
};

let Ok(char_count_to_l_parens) = u32::try_from(char_count) else {
return FuncSpace::NotSpaced;
};

let l_parens_pos = search_span.start + char_count_to_l_parens;
let span_to_l_parens = Span::new(search_span.start, l_parens_pos);
let src_to_l_parens = ctx.source_range(span_to_l_parens);

if src_to_l_parens.is_empty() {
return FuncSpace::NotSpaced;
}

if src_to_l_parens.trim().is_empty() {
FuncSpace::Spaced(span_to_l_parens)
} else {
FuncSpace::NotSpaced
}
}

fn check_ident_to_application(ctx: &LintContext, span: Span) {
match get_substring_to_lparens(ctx, span) {
FuncSpace::NotSpaced => {}
FuncSpace::Spaced(span) => {
ctx.diagnostic_with_fix(no_spaced_func_diagnostic(span), |fixer| fixer.delete(&span));
}
}
}

impl Rule for NoSpacedFunc {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::NewExpression(new_expr) = node.kind() {
// new Foo ()
let callee = new_expr.callee.without_parentheses();
let callee_end = callee.span().end;
let span = Span::new(callee_end, new_expr.span().end);

check_ident_to_application(ctx, span);
};

let AstKind::CallExpression(call_expr) = node.kind() else {
return;
};

let callee_end = call_expr.span().end;

match &call_expr.callee {
// f ()
Expression::Identifier(ident) => {
let span = Span::new(ident.span().end, callee_end);
check_ident_to_application(ctx, span);
}
Expression::ParenthesizedExpression(paren_exp) => {
if let Expression::Identifier(_ident) = &paren_exp.expression {
let span = Span::new(paren_exp.span().end, callee_end);
check_ident_to_application(ctx, span);
}
}
// f() () <-- second parens
Expression::CallExpression(c) => {
let span = Span::new(c.span().end, callee_end);
check_ident_to_application(ctx, span);
}
// f.a ()
Expression::StaticMemberExpression(exp) => {
let ident_name_span_end = exp.property.span().end;
let span = Span::new(ident_name_span_end, callee_end);

check_ident_to_application(ctx, span);
}
// function(){ } ()
Expression::FunctionExpression(func) => {
let span = Span::new(func.span().end, callee_end);
check_ident_to_application(ctx, span);
}
_ => {}
}
}
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
"foo(1);",
"f();",
"f( );",
"f(a, b);",
"a.b",
"a. b",
"f.b();",
"f.b().c();",
"f.b(1).c( );",
"f()()",
"f()( )()",
"(function() {}())",
"var f = new Foo()",
"var f = new Foo",
"f( (0) )",
"( f )( 0 )",
"( (f) )( (0) )",
"( f()() )(0)",
"(function(){ if (foo) { bar(); } }());",
"f(0, (1))",
"describe/**/('foo', function () {});",
"new (foo())",
];

let fail = vec![
"f ();",
"f (a, b);",
"f
();",
"f.b ();",
"f.b().c ();",
"f.b().c().d ();",
"f() ()",
"f()() ()",
"(function() {} ())",
"var f = new Foo ()",
"f ( (0) )",
"f(0) (1)",
"(f) (0)",
"f ();
t ();",
];

let fix = vec![
("f ();", "f();", None),
("f (a, b);", "f(a, b);", None),
(
"f
();",
"f();",
None,
),
("f.b ();", "f.b();", None),
("f.b().c ();", "f.b().c();", None),
("f() ()", "f()()", None),
("(function() {} ())", "(function() {}())", None),
("var f = new Foo ()", "var f = new Foo()", None),
("f ( (0) )", "f( (0) )", None),
("f(0) (1)", "f(0)(1)", None),
("(f) (0)", "(f)(0)", None),
(
"f ();
t ();",
"f();
t();",
None,
),
];

Tester::new(NoSpacedFunc::NAME, NoSpacedFunc::PLUGIN, pass, fail)
.expect_fix(fix)
.test_and_snapshot();
}
109 changes: 109 additions & 0 deletions crates/oxc_linter/src/snapshots/eslint_no_spaced_func.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
source: crates/oxc_linter/src/tester.rs
---
eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:2]
1 │ f ();
· ─
╰────
help: Delete this code.

eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:2]
1 │ f (a, b);
· ─
╰────
help: Delete this code.

eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:2]
1 │ ╭─▶ f
2 │ ╰─▶ ();
╰────
help: Delete this code.

eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:4]
1 │ f.b ();
· ─
╰────
help: Delete this code.

eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:8]
1 │ f.b().c ();
· ─
╰────
help: Delete this code.

eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:12]
1 │ f.b().c().d ();
· ─
╰────
help: Delete this code.

eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:4]
1 │ f() ()
· ─
╰────
help: Delete this code.

⚠ eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:6]
1 │ f()() ()
· ─
╰────
help: Delete this code.

⚠ eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:15]
1 │ (function() {} ())
· ─
╰────
help: Delete this code.

eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:16]
1 │ var f = new Foo ()
· ─
╰────
help: Delete this code.

⚠ eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:2]
1 │ f ( (0) )
· ─
╰────
help: Delete this code.

⚠ eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:5]
1 │ f(0) (1)
· ─
╰────
help: Delete this code.

⚠ eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:4]
1 │ (f) (0)
· ─
╰────
help: Delete this code.

⚠ eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:1:2]
1 │ f ();
· ─
2t ();
╰────
help: Delete this code.

eslint(no-spaced-func): Spacing between function identifiers and their applications is disallowed.
╭─[no_spaced_func.tsx:2:6]
1 │ f ();
2t ();
· ───
╰────
help: Delete this code.

0 comments on commit d38e6de

Please sign in to comment.