Skip to content

Commit

Permalink
feat(linter): eslint-plugin-unicorn(prefer-array-flat-map) (#997)
Browse files Browse the repository at this point in the history
  • Loading branch information
camc314 authored Oct 16, 2023
1 parent 0f72066 commit 952139c
Show file tree
Hide file tree
Showing 3 changed files with 233 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 @@ -131,6 +131,7 @@ mod unicorn {
pub mod no_instanceof_array;
pub mod no_thenable;
pub mod no_unnecessary_await;
pub mod prefer_array_flat_map;
}

oxc_macros::declare_all_lint_rules! {
Expand Down Expand Up @@ -239,6 +240,7 @@ oxc_macros::declare_all_lint_rules! {
unicorn::no_unnecessary_await,
unicorn::no_thenable,
unicorn::filename_case,
unicorn::prefer_array_flat_map,
import::named,
import::no_cycle,
import::no_self_import,
Expand Down
135 changes: 135 additions & 0 deletions crates/oxc_linter/src/rules/unicorn/prefer_array_flat_map.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use oxc_ast::{
ast::{Argument, Expression},
AstKind,
};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

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

#[derive(Debug, Error, Diagnostic)]
#[error("eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.")]
#[diagnostic(severity(warning), help("Prefer `.flatMap(…)` over `.map(…).flat()`."))]
struct PreferArrayFlatMapDiagnostic(#[label] pub Span);

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

declare_oxc_lint!(
/// ### What it does
///
/// Prefers the use of `.flatMap()` when `map().flat()` are used together.
///
/// ### Why is this bad?
///
/// It is slightly more efficient to use `.flatMap(…)` instead of `.map(…).flat()`.
///
/// ### Example
/// ```javascript
/// const bar = [1,2,3].map(i => [i]).flat(); // ✗ fail
///
/// const bar = [1,2,3].flatMap(i => [i]); // ✓ pass
/// ```
PreferArrayFlatMap,
correctness
);

impl Rule for PreferArrayFlatMap {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::CallExpression(flat_call_expr) = node.kind() else { return };

if flat_call_expr.arguments.len() > 1 {
return;
}

let callee = &flat_call_expr.callee.without_parenthesized();

if let Expression::MemberExpression(v) = callee {
if let Some(static_property_name) = v.static_property_name() {
if static_property_name != "flat" {
return;
}
}
}

if let Some(first_arg) = flat_call_expr.arguments.first() {
if let Argument::Expression(Expression::NumberLiteral(number_lit)) = first_arg {
if number_lit.raw != "1" {
return;
}
} else {
return;
}
}

let Expression::MemberExpression(member_expr) = callee else { return };

let Expression::CallExpression(map_call_expr) =
member_expr.object().without_parenthesized()
else {
return;
};

if let Expression::MemberExpression(map_member_expr) =
&map_call_expr.callee.without_parenthesized()
{
if let Some(property_name) = map_member_expr.static_property_name() {
if property_name != "map" {
return;
}
}
};

ctx.diagnostic(PreferArrayFlatMapDiagnostic(flat_call_expr.span));
}
}

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

let pass = vec![
("const bar = [1,2,3].map()", None),
("const bar = [1,2,3].map(i => i)", None),
("const bar = [1,2,3].map((i) => i)", None),
("const bar = [1,2,3].map((i) => { return i; })", None),
("const bar = foo.map(i => i)", None),
("const bar = [[1],[2],[3]].flat()", None),
("const bar = [1,2,3].map(i => [i]).sort().flat()", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(2)", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(1, null)", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(Infinity)", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(Number.POSITIVE_INFINITY)", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(Number.MAX_VALUE)", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(Number.MAX_SAFE_INTEGER)", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(...[1])", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(0.4 +.6)", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(+1)", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(foo)", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(foo.bar)", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(1.00)", None),
];

let fail = vec![
("const bar = [[1],[2],[3]].map(i => [i]).flat()", None),
("const bar = [[1],[2],[3]].map(i => [i]).flat(1,)", None),
("const bar = [1,2,3].map(i => [i]).flat()", None),
("const bar = [1,2,3].map((i) => [i]).flat()", None),
("const bar = [1,2,3].map((i) => { return [i]; }).flat()", None),
("const bar = [1,2,3].map(foo).flat()", None),
("const bar = foo.map(i => [i]).flat()", None),
("const bar = { map: () => {} }.map(i => [i]).flat()", None),
("const bar = [1,2,3].map(i => i).map(i => [i]).flat()", None),
("const bar = [1,2,3].sort().map(i => [i]).flat()", None),
("const bar = (([1,2,3].map(i => [i]))).flat()", None),
("let bar = [1,2,3] . map( x => y ) . flat () // 🤪", None),
("const bar = [1,2,3].map(i => [i]).flat(1);", None),
];

Tester::new(PreferArrayFlatMap::NAME, pass, fail).test_and_snapshot();
}
96 changes: 96 additions & 0 deletions crates/oxc_linter/src/snapshots/prefer_array_flat_map.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
source: crates/oxc_linter/src/tester.rs
expression: prefer_array_flat_map
---
eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1const bar = [[1],[2],[3]].map(i => [i]).flat()
· ──────────────────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.

eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1const bar = [[1],[2],[3]].map(i => [i]).flat(1,)
· ────────────────────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.

eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1const bar = [1,2,3].map(i => [i]).flat()
· ────────────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.

eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1const bar = [1,2,3].map((i) => [i]).flat()
· ──────────────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.

eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1const bar = [1,2,3].map((i) => { return [i]; }).flat()
· ──────────────────────────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.

eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1const bar = [1,2,3].map(foo).flat()
· ───────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.

eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1const bar = foo.map(i => [i]).flat()
· ────────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.

eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1const bar = { map: () => {} }.map(i => [i]).flat()
· ──────────────────────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.

eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1const bar = [1,2,3].map(i => i).map(i => [i]).flat()
· ────────────────────────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.

eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1const bar = [1,2,3].sort().map(i => [i]).flat()
· ───────────────────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.

eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1const bar = (([1,2,3].map(i => [i]))).flat()
· ────────────────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.

eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1let bar = [1,2,3] . map( x => y ) . flat () // 🤪
· ─────────────────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.

eslint-plugin-unicorn(prefer-array-flat-map): `Array.flatMap` performs `Array.map` and `Array.flat` in one step.
╭─[prefer_array_flat_map.tsx:1:1]
1const bar = [1,2,3].map(i => [i]).flat(1);
· ─────────────────────────────
╰────
help: Prefer `.flatMap(…)` over `.map(…).flat()`.


0 comments on commit 952139c

Please sign in to comment.