Repository: oxc-project/oxc
Pull request #11174: feat(linter): implement unicorn/prefer-object-from-entries
Author: login: camc314
Labels: name: C-enhancement
description: Category - New feature or request
name: A-linter
description: Area - Linter
Created: 2025-05-19T20:07:51Z | Merged: 2025-05-19T22:20:24Z



Diff:
diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index dfe64923c344e..6eb523747b0c2 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -409,6 +409,7 @@ mod unicorn {
     pub mod prefer_negative_index;
     pub mod prefer_node_protocol;
     pub mod prefer_number_properties;
+    pub mod prefer_object_from_entries;
     pub mod prefer_optional_catch_binding;
     pub mod prefer_prototype_methods;
     pub mod prefer_query_selector;
@@ -1034,6 +1035,7 @@ oxc_macros::declare_all_lint_rules! {
     unicorn::no_zero_fractions,
     unicorn::number_literal_case,
     unicorn::numeric_separators_style,
+    unicorn::prefer_object_from_entries,
     unicorn::prefer_array_find,
     unicorn::prefer_array_index_of,
     unicorn::prefer_spread,
diff --git a/crates/oxc_linter/src/rules/unicorn/no_array_method_this_argument.rs b/crates/oxc_linter/src/rules/unicorn/no_array_method_this_argument.rs
index 4b8afb08c2228..7a7383e42c74d 100644
--- a/crates/oxc_linter/src/rules/unicorn/no_array_method_this_argument.rs
+++ b/crates/oxc_linter/src/rules/unicorn/no_array_method_this_argument.rs
@@ -1,12 +1,15 @@
 use oxc_ast::{
     AstKind,
-    ast::{Argument, Expression, MemberExpression},
+    ast::{Argument, Expression},
 };
 use oxc_diagnostics::OxcDiagnostic;
 use oxc_macros::declare_oxc_lint;
 use oxc_span::{GetSpan, Span};
 
-use crate::{AstNode, ast_util::is_method_call, context::LintContext, rule::Rule};
+use crate::{
+    AstNode, ast_util::is_method_call, context::LintContext, rule::Rule,
+    utils::does_expr_match_any_path,
+};
 
 fn no_array_method_this_argument_diagnostic(span: Span) -> OxcDiagnostic {
     OxcDiagnostic::warn("Avoid using 'thisArg' with array iteration methods")
@@ -83,7 +86,7 @@ fn check_array_prototype_methods<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>)
         .first()
         .is_some_and(|arg| arg.as_expression().is_none_or(|expr| is_node_not_function(expr)))
         || call_expr.arguments.get(1).is_some_and(|arg| matches!(arg, Argument::SpreadElement(_)))
-        || is_ignored_expr(&call_expr.callee)
+        || does_expr_match_any_path(&call_expr.callee, IGNORED)
     {
         return;
     }
@@ -112,31 +115,6 @@ fn check_array_from<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) {
     ));
 }
 
-fn is_ignored_expr(mut expr: &Expression) -> bool {
-    let mut path = Vec::new();
-
-    while let Some(member_expr) = expr.as_member_expression() {
-        let MemberExpression::StaticMemberExpression(static_mem_expr) = member_expr else {
-            return false;
-        };
-        path.push(static_mem_expr.property.name.as_str());
-
-        expr = member_expr.object().get_inner_expression();
-    }
-
-    let Expression::Identifier(ident) = expr else { return false };
-    path.push(ident.name.as_str());
-    let path = path.iter().rev();
-
-    for e in IGNORED {
-        if e.iter().zip(path.clone()).all(|(x, y)| x == y) {
-            return true;
-        }
-    }
-
-    false
-}
-
 fn is_node_not_function(expr: &Expression) -> bool {
     match expr {
         Expression::ArrayExpression(_)
diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_array_find.rs b/crates/oxc_linter/src/rules/unicorn/prefer_array_find.rs
index 5f5b5f61478d5..e0f4cf4f9ac90 100644
--- a/crates/oxc_linter/src/rules/unicorn/prefer_array_find.rs
+++ b/crates/oxc_linter/src/rules/unicorn/prefer_array_find.rs
@@ -9,7 +9,10 @@ use oxc_diagnostics::OxcDiagnostic;
 use oxc_macros::declare_oxc_lint;
 use oxc_span::{GetSpan, Span};
 
-use crate::{AstNode, ast_util::is_method_call, context::LintContext, rule::Rule};
+use crate::{
+    AstNode, ast_util::is_method_call, context::LintContext, rule::Rule,
+    utils::call_expr_member_expr_property_span,
+};
 
 fn prefer_array_find_diagnostic(span: Span) -> OxcDiagnostic {
     OxcDiagnostic::warn("Prefer 'find' over filtering and accessing the first result")
@@ -260,14 +263,6 @@ fn is_left_hand_side<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> bool {
     false
 }
 
-fn call_expr_member_expr_property_span(call_expr: &CallExpression) -> Span {
-    call_expr
-        .callee
-        .as_member_expression()
-        .and_then(oxc_ast::ast::MemberExpression::static_property_info)
-        .map_or(call_expr.span, |(span, _)| span)
-}
-
 #[test]
 fn test() {
     use crate::tester::Tester;
diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_object_from_entries.rs b/crates/oxc_linter/src/rules/unicorn/prefer_object_from_entries.rs
new file mode 100644
index 0000000000000..d64f990e6069f
--- /dev/null
+++ b/crates/oxc_linter/src/rules/unicorn/prefer_object_from_entries.rs
@@ -0,0 +1,430 @@
+use oxc_ast::{
+    AstKind,
+    ast::{Expression, ObjectPropertyKind, PropertyKind, Statement},
+};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::{GetSpan, Span};
+use serde::Deserialize;
+
+use crate::{
+    AstNode,
+    ast_util::is_method_call,
+    context::LintContext,
+    rule::Rule,
+    utils::{call_expr_member_expr_property_span, does_expr_match_any_path},
+};
+
+fn prefer_object_from_entries_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Prefer 'Object.fromEntries' over manual object construction from entries")
+        .with_help("Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct PreferObjectFromEntries(Box<PreferObjectFromEntriesConfig>);
+
+#[derive(Debug, Default, Clone, Deserialize)]
+pub struct PreferObjectFromEntriesConfig {
+    functions: Vec<String>,
+}
+
+impl std::ops::Deref for PreferObjectFromEntries {
+    type Target = PreferObjectFromEntriesConfig;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Encourages using `Object.fromEntries` when converting an array of key-value pairs
+    /// into an object.
+    ///
+    /// ### Why is this bad?
+    ///
+    /// Manually constructing objects from key-value pairs using `reduce` or `forEach`
+    /// is more verbose, error-prone, and harder to understand. The `Object.fromEntries`
+    /// method is clearer, more declarative, and built for exactly this purpose.
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// const result = pairs.reduce((obj, [key, value]) => {
+    ///   obj[key] = value;
+    ///   return obj;
+    /// }, {});
+    ///
+    /// const result = {};
+    /// pairs.forEach(([key, value]) => {
+    ///   result[key] = value;
+    /// });
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// const result = Object.fromEntries(pairs);
+    /// ```
+    PreferObjectFromEntries,
+    unicorn,
+    style,
+    pending
+);
+
+impl Rule for PreferObjectFromEntries {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::CallExpression(call_expr) = node.kind() else { return };
+
+        if call_expr.arguments.len() == 1
+            && call_expr.arguments[0].is_expression()
+            && does_expr_match_any_path(
+                &call_expr.callee,
+                self.functions.iter().map(|fun| fun.split('.').collect::<Vec<_>>()),
+            )
+        {
+            ctx.diagnostic(prefer_object_from_entries_diagnostic(call_expr.callee.span()));
+            return;
+        }
+
+        if !is_method_call(call_expr, None, Some(&["reduce"]), Some(2), Some(2)) {
+            return;
+        }
+
+        if call_expr.optional || call_expr.callee.to_member_expression().optional() {
+            return;
+        }
+
+        let Some(accumulator) = call_expr
+            .arguments
+            .get(1)
+            .expect("call expr must have exactly 2 arguments")
+            .as_expression()
+            .map(oxc_ast::ast::Expression::get_inner_expression)
+        else {
+            return;
+        };
+
+        if !is_empty_object(accumulator) {
+            return;
+        }
+
+        let Some(Expression::ArrowFunctionExpression(reducer)) = call_expr
+            .arguments
+            .first()
+            .expect("call expr must have exactly 2 arguments")
+            .as_expression()
+            .map(oxc_ast::ast::Expression::get_inner_expression)
+        else {
+            return;
+        };
+
+        if !reducer.expression || reducer.r#async {
+            return;
+        }
+
+        let Statement::ExpressionStatement(stmt) = reducer
+            .body
+            .statements
+            .first()
+            .expect("arrow function expressions must have at least one body statement")
+        else {
+            return;
+        };
+        let stmt = stmt.expression.get_inner_expression();
+
+        let Some(accumulator_ident) =
+            reducer.params.items.first().and_then(|arg| arg.pattern.get_binding_identifier())
+        else {
+            return;
+        };
+
+        if ctx.symbol_references(accumulator_ident.symbol_id()).count() != 1 {
+            return;
+        }
+
+        // `() => Object.assign(object, {key})`
+        if let Expression::CallExpression(call_expr) = &stmt {
+            if !is_method_call(call_expr, Some(&["Object"]), Some(&["assign"]), Some(2), Some(2)) {
+                return;
+            }
+
+            let Some(Expression::Identifier(target)) = call_expr
+                .arguments
+                .first()
+                .expect("call expression should have 2 arguments")
+                .as_expression()
+                .map(oxc_ast::ast::Expression::get_inner_expression)
+            else {
+                return;
+            };
+
+            if ctx.scoping().get_reference(target.reference_id()).symbol_id()
+                != Some(accumulator_ident.symbol_id())
+            {
+                return;
+            }
+
+            let Some(Expression::ObjectExpression(source)) = call_expr
+                .arguments
+                .get(1)
+                .expect("call expression should have 2 arguments")
+                .as_expression()
+                .map(oxc_ast::ast::Expression::get_inner_expression)
+            else {
+                return;
+            };
+
+            if source.properties.len() != 1 {
+                return;
+            }
+
+            let ObjectPropertyKind::ObjectProperty(k) = &source.properties[0] else { return };
+            if k.kind != PropertyKind::Init || k.method {
+                return;
+            }
+
+            ctx.diagnostic(prefer_object_from_entries_diagnostic(
+                call_expr_member_expr_property_span(call_expr),
+            ));
+        }
+
+        // `() => ({...object, key})`
+        if let Expression::ObjectExpression(object_expr) = &stmt {
+            if object_expr.properties.len() == 2 {
+                if let ObjectPropertyKind::SpreadProperty(spread) = &object_expr.properties[0] {
+                    if let Expression::Identifier(spread_ident) =
+                        spread.argument.get_inner_expression()
+                    {
+                        let Some(spread_symbol_id) =
+                            ctx.scoping().get_reference(spread_ident.reference_id()).symbol_id()
+                        else {
+                            return;
+                        };
+
+                        if spread_symbol_id != accumulator_ident.symbol_id() {
+                            return;
+                        }
+                        let ObjectPropertyKind::ObjectProperty(object_prop) =
+                            &object_expr.properties[1]
+                        else {
+                            return;
+                        };
+
+                        if object_prop.kind != PropertyKind::Init || object_prop.method {
+                            return;
+                        }
+
+                        ctx.diagnostic(prefer_object_from_entries_diagnostic(
+                            call_expr_member_expr_property_span(call_expr),
+                        ));
+                    }
+                }
+            }
+        }
+    }
+
+    fn from_configuration(value: serde_json::Value) -> Self {
+        let mut config: PreferObjectFromEntriesConfig = value
+            .as_array()
+            .and_then(|arr| arr.first())
+            .map(|config| {
+                serde_json::from_value(config.clone()).expect("Failed to deserialize config")
+            })
+            .unwrap_or_default();
+
+        config.functions.push("_.fromPairs".into());
+        config.functions.push("lodash.fromPairs".into());
+
+        Self(Box::new(config))
+    }
+}
+
+fn is_empty_object(expr: &Expression) -> bool {
+    match expr {
+        Expression::ObjectExpression(o) if o.properties.is_empty() => true,
+        Expression::CallExpression(call_expr)
+            if is_method_call(
+                call_expr,
+                Some(&["Object"]),
+                Some(&["create"]),
+                Some(1),
+                Some(1),
+            ) && call_expr
+                .arguments
+                .first()
+                .expect("call expression must have 1 argument")
+                .as_expression()
+                .map(oxc_ast::ast::Expression::get_inner_expression)
+                .is_some_and(|expr| matches!(expr, Expression::NullLiteral(_))) =>
+        {
+            true
+        }
+        _ => false,
+    }
+}
+
+#[test]
+fn test() {
+    use crate::tester::Tester;
+
+    let pass = vec![
+        ("pairs.reduce(object => ({...object, key}));", None),
+        ("pairs.reduce(object => ({...object, key}), {}, extraArgument);", None),
+        ("pairs.reduce({}, object => ({...object, key}));", None),
+        ("reduce(object => ({...object, key}), {});", None),
+        ("new reduce(object => ({...object, key}), {});", None),
+        ("pairs.reduce?.(object => ({...object, key}), {});", None),
+        ("pairs?.reduce(object => ({...object, key}), {});", None),
+        ("pairs.notReduce(object => ({...object, key}), {});", None),
+        ("pairs.reduce(object => ({...object, key}), {notEmpty});", None),
+        ("pairs.reduce(object => ({...object, key}), []);", None),
+        ("pairs.reduce(object => ({...object, key}), {}, extraArgument);", None),
+        ("pairs.reduce(...[(object => ({...object, key}))], {});", None),
+        ("pairs.reduce(object => ({...object, key}), ...[{}]);", None),
+        ("pairs.reduce(object => ({...object, key}), Object.create());", None),
+        ("pairs.reduce(object => ({...object, key}), Object.create(null, extraArgument));", None),
+        ("pairs.reduce(object => ({...object, key}), Object.create?.(null));", None),
+        ("pairs.reduce(object => ({...object, key}), Object?.create(null));", None),
+        ("pairs.reduce(object => ({...object, key}), window.Object.create(null));", None),
+        ("pairs.reduce(object => ({...object, key}), Object.notCreate(null));", None),
+        ("pairs.reduce(object => ({...object, key}), NotObject.create(null));", None),
+        ("pairs.reduce(object => ({...object, key}), object.create(null));", None),
+        ("pairs.reduce(object => ({...object, key}), object.CREATE(null));", None),
+        (r#"pairs.reduce(object => ({...object, key}), Object.create("null"));"#, None),
+        ("pairs.reduce(callback, {})", None),
+        ("pairs.reduce(callback, Object.create(null))", None),
+        ("pairs.reduce(async function * () {}, {})", None),
+        ("pairs.reduce()", None),
+        ("pairs.reduce(callback, {}, extraArgument)", None),
+        ("pairs.reduce?.(callback, {})", None),
+        ("pairs?.reduce(callback, {})", None),
+        ("pairs.notReduce(callback, {})", None),
+        ("pairs[reduce](callback, {})", None),
+        ("pairs.reduce(...callback, {})", None),
+        ("pairs.reduce(function(object) {Object.assign(object, {key})}, {});", None),
+        ("pairs.reduce(object => ({...object, key} + 1), {});", None),
+        //("pairs.reduce((object = {}) => ({...object, key}), {});", None),
+        ("pairs.reduce((object) => ({...NotSameObject, key}), {});", None),
+        ("pairs.reduce(object => ({...object, key, anotherKey}), {});", None),
+        ("pairs.reduce(object => ({}), {});", None),
+        ("pairs.reduce(object => ({keyFirst, ...object}), {});", None),
+        ("pairs.reduce(async object => ({...object, key}), {});", None),
+        ("pairs.reduce(async object => await {...object, key}, {});", None),
+        ("pairs.reduce((...object) => ({...object, key}), {});", None),
+        ("pairs.reduce(({object}) => ({...object, key}), {});", None),
+        ("pairs.reduce(object => ({...object, ...key}), {});", None),
+        ("pairs.reduce(object => Object.assign(NotSameObject, {key}), {});", None),
+        ("pairs.reduce(object => Object.assign(object, {}), {});", None),
+        ("pairs.reduce(object => Object.assign(object, {...key}), {});", None),
+        ("pairs.reduce(object => Object.assign?.(object, {key}), {});", None),
+        ("pairs.reduce(object => Object?.assign(object, {key}), {});", None),
+        ("pairs.reduce(object => Object.notAssign(object, {key}), {});", None),
+        ("pairs.reduce(object => NotObject.assign(object, {key}), {});", None),
+        ("pairs.reduce(object => ({...object, object}), {});", None),
+        ("pairs.reduce(object => ({...object, key: Object.keys(object)}), {});", None),
+        ("pairs.reduce((object, [key, value = object]) => ({...object, [key]: value}), {});", None),
+        ("pairs.reduce((object) => Object.assign(object, {object}), {});", None),
+        ("pairs.reduce(object => ({...object, key: function () { return object; }}), {});", None),
+        ("pairs.reduce(object => ({...object, method() {}}), {});", None),
+        ("pairs.reduce(object => Object.assign(object, {async * method() {}}), {});", None),
+        ("pairs.reduce(object => ({...object, async method() {}}), {});", None),
+        ("pairs.reduce(object => ({...object, * method() {}}), {});", None),
+        ("pairs.reduce(object => ({...object, async * method() {}}), {});", None),
+        ("pairs.reduce(object => ({...object, get key() {}}), {});", None),
+        ("pairs.reduce(object => ({...object, set key(v) {}}), {});", None),
+        (
+            "const flattened = arrayOfObjects.reduce((flattened, next) => Object.assign(flattened, next), {});",
+            None,
+        ),
+        ("underscore.fromPairs(pairs)", None),
+        ("_.fromPairs", None),
+        ("_.fromPairs()", None),
+        ("new _.fromPairs(pairs)", None),
+        ("_.fromPairs(...[pairs])", None),
+        ("_.foo(pairs)", Some(serde_json::json!([{"functions": ["foo"]}]))),
+        ("foo(pairs)", Some(serde_json::json!([{"functions": ["utils.object.foo"]}]))),
+        ("object.foo(pairs)", Some(serde_json::json!([{"functions": ["utils.object.foo"]}]))),
+    ];
+
+    let fail = vec![
+        ("pairs.reduce(object => ({...object, key}), {});", None),
+        ("pairs.reduce(object => ({...object, key}), {},);", None),
+        ("pairs.reduce(object => ({...object, key,}), {});", None),
+        ("pairs.reduce(object => ({...object, key}), Object.create(null));", None),
+        ("pairs.reduce(object => ({...object, key}), Object.create(null),);", None),
+        ("pairs.reduce(object => ({...object, key}), (( {} )));", None),
+        ("pairs.reduce(object => ({...object, key}), (( Object.create(null) )),);", None),
+        ("pairs.reduce( (( object => ({...object, key}) )) , {});", None),
+        ("pairs.reduce( (( (object) => ({...object, key}) )) , {});", None),
+        ("pairs.reduce( (( (object,) => ({...object, key}) )) , {});", None),
+        ("pairs.reduce(object => ({...object, [((key))] : ((value))}), {});", None),
+        (
+            "((
+				(( pairs ))
+				.reduce(
+					((
+						(object,) => ((
+							((
+								Object
+							)).assign(
+								((
+									object
+								)),
+								(({
+									[ ((key)) ] : ((value)),
+								}))
+							)
+						))
+					)),
+					Object.create(((null)),)
+				)
+			));",
+            None,
+        ),
+        ("pairs.reduce(object => ({...object, 0: value}), {});", None),
+        ("pairs.reduce(object => ({...object, true: value}), {});", None),
+        ("pairs.reduce(object => ({...object, 0n: value}), {});", None),
+        ("pairs.reduce(object => ({...object, undefined: value}), {});", None),
+        ("pairs.reduce(object => ({...object, null: value}), {});", None),
+        ("pairs.reduce(object => ({...object, var: value}), {});", None),
+        ("pairs.reduce(object => ({...object, for: value}), {});", None),
+        ("pairs.reduce(object => ({...object, default: value}), {});", None),
+        ("pairs.reduce(object => ({...object, string: value}), {});", None),
+        (r#"pairs.reduce(object => ({...object, "string": value}), {});"#, None),
+        ("pairs.reduce(object => ({...object, [0]: value}), {});", None),
+        ("pairs.reduce(object => ({...object, [true]: value}), {});", None),
+        ("pairs.reduce(object => ({...object, [0n]: value}), {});", None),
+        ("pairs.reduce(object => ({...object, [undefined]: value}), {});", None),
+        ("pairs.reduce(object => ({...object, [null]: value}), {});", None),
+        (r#"pairs.reduce(object => ({...object, ["for"]: value}), {});"#, None),
+        ("pairs.reduce(object => ({...object, [string]: value}), {});", None),
+        (r#"pairs.reduce(object => ({...object, ["string"]: value}), {});"#, None),
+        ("pairs.reduce(object => Object.assign(object, {key}), {});", None),
+        ("pairs.reduce(object => Object.assign(object, {key,}), {});", None),
+        ("pairs.reduce(object => Object.assign(object, {[key]: value,}), {});", None),
+        ("pairs.reduce((object, element, index, array) => ({...object, key}), {});", None),
+        (
+            "pairs.reduce((object, [key, value], index, array,) => ({...object, [key]: value + index + array.length}), {});",
+            None,
+        ),
+        (
+            "pairs.reduce(object => ({...object, key: function (object) { return object; }}), {});",
+            None,
+        ),
+        ("pairs.reduce(object => ({...object, method: async () => {}}), {});", None),
+        ("pairs.reduce(object => ({...object, method: async function * (){}}), {});", None),
+        ("_.fromPairs(pairs)", None),
+        ("lodash.fromPairs(pairs)", None),
+        (
+            "myFromPairsFunction(pairs)",
+            Some(serde_json::json!([{"functions": ["myFromPairsFunction"]}])),
+        ),
+        ("utils.object.foo(pairs)", Some(serde_json::json!([{"functions": ["utils.object.foo"]}]))),
+    ];
+
+    Tester::new(PreferObjectFromEntries::NAME, PreferObjectFromEntries::PLUGIN, pass, fail)
+        .test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/unicorn_prefer_object_from_entries.snap b/crates/oxc_linter/src/snapshots/unicorn_prefer_object_from_entries.snap
new file mode 100644
index 0000000000000..15e62e05507c4
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/unicorn_prefer_object_from_entries.snap
@@ -0,0 +1,298 @@
+---
+source: crates/oxc_linter/src/tester.rs
+---
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, key}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, key}), {},);
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, key,}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, key}), Object.create(null));
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, key}), Object.create(null),);
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, key}), (( {} )));
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, key}), (( Object.create(null) )),);
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce( (( object => ({...object, key}) )) , {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce( (( (object) => ({...object, key}) )) , {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce( (( (object,) => ({...object, key}) )) , {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, [((key))] : ((value))}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:8:11]
+ 7 │                                 Object
+ 8 │                             )).assign(
+   ·                                ──────
+ 9 │                                 ((
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, 0: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, true: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, 0n: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, undefined: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, null: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, var: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, for: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, default: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, string: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, "string": value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, [0]: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, [true]: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, [0n]: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, [undefined]: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, [null]: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, ["for"]: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, [string]: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, ["string"]: value}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:31]
+ 1 │ pairs.reduce(object => Object.assign(object, {key}), {});
+   ·                               ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:31]
+ 1 │ pairs.reduce(object => Object.assign(object, {key,}), {});
+   ·                               ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:31]
+ 1 │ pairs.reduce(object => Object.assign(object, {[key]: value,}), {});
+   ·                               ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce((object, element, index, array) => ({...object, key}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce((object, [key, value], index, array,) => ({...object, [key]: value + index + array.length}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, key: function (object) { return object; }}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, method: async () => {}}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:7]
+ 1 │ pairs.reduce(object => ({...object, method: async function * (){}}), {});
+   ·       ──────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:1]
+ 1 │ _.fromPairs(pairs)
+   · ───────────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:1]
+ 1 │ lodash.fromPairs(pairs)
+   · ────────────────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:1]
+ 1 │ myFromPairsFunction(pairs)
+   · ───────────────────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
+
+  ⚠ eslint-plugin-unicorn(prefer-object-from-entries): Prefer 'Object.fromEntries' over manual object construction from entries
+   ╭─[prefer_object_from_entries.tsx:1:1]
+ 1 │ utils.object.foo(pairs)
+   · ────────────────
+   ╰────
+  help: Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach
diff --git a/crates/oxc_linter/src/utils/unicorn.rs b/crates/oxc_linter/src/utils/unicorn.rs
index dfee4157a7f63..6a3587c72decb 100644
--- a/crates/oxc_linter/src/utils/unicorn.rs
+++ b/crates/oxc_linter/src/utils/unicorn.rs
@@ -2,12 +2,12 @@ mod boolean;
 use oxc_ast::{
     AstKind,
     ast::{
-        BindingPatternKind, Expression, FormalParameters, FunctionBody, LogicalExpression,
-        MemberExpression, Statement,
+        BindingPatternKind, CallExpression, Expression, FormalParameters, FunctionBody,
+        LogicalExpression, MemberExpression, Statement,
     },
 };
 use oxc_semantic::AstNode;
-use oxc_span::ContentEq;
+use oxc_span::{ContentEq, Span};
 use oxc_syntax::operator::LogicalOperator;
 
 pub use self::boolean::*;
@@ -328,3 +328,40 @@ pub fn is_same_member_expression(
         ctx,
     )
 }
+
+pub fn call_expr_member_expr_property_span(call_expr: &CallExpression) -> Span {
+    call_expr
+        .callee
+        .as_member_expression()
+        .and_then(oxc_ast::ast::MemberExpression::static_property_info)
+        .map_or(call_expr.span, |(span, _)| span)
+}
+
+pub fn does_expr_match_any_path<'a, P, S>(mut expr: &Expression, paths: P) -> bool
+where
+    P: IntoIterator<Item = S>,
+    S: AsRef<[&'a str]>,
+{
+    let mut path = Vec::new();
+
+    while let Some(member_expr) = expr.as_member_expression() {
+        let MemberExpression::StaticMemberExpression(static_mem_expr) = member_expr else {
+            return false;
+        };
+        path.push(static_mem_expr.property.name.as_str());
+
+        expr = member_expr.object().get_inner_expression();
+    }
+
+    let Expression::Identifier(ident) = expr else { return false };
+    path.push(ident.name.as_str());
+    let path = path.iter().rev();
+
+    for e in paths {
+        if e.as_ref().iter().zip(path.clone()).all(|(x, y)| x == y) {
+            return true;
+        }
+    }
+
+    false
+}


Reviews:
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T20:12:34Z

Review comments:
diff_hunk: @@ -0,0 +1,430 @@
+use oxc_ast::{
+    AstKind,
+    ast::{Expression, ObjectPropertyKind, PropertyKind, Statement},
+};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::{GetSpan, Span};
+use serde::Deserialize;
+
+use crate::{
+    AstNode,
+    ast_util::is_method_call,
+    context::LintContext,
+    rule::Rule,
+    utils::{call_expr_member_expr_property_span, does_expr_match_any_path},
+};
+
+fn prefer_object_from_entries_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Prefer 'Object.fromEntries' over manual object construction from entries")
+        .with_help("Use 'Object.fromEntries(pairs)' instead of manually building objects with reduce or forEach")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct PreferObjectFromEntries(Box<PreferObjectFromEntriesConfig>);
+
+#[derive(Debug, Default, Clone, Deserialize)]
+pub struct PreferObjectFromEntriesConfig {
+    functions: Vec<String>,
+}
+
+impl std::ops::Deref for PreferObjectFromEntries {
+    type Target = PreferObjectFromEntriesConfig;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Encourages using `Object.fromEntries` when converting an array of key-value pairs
+    /// into an object.
+    ///
+    /// ### Why is this bad?
+    ///
+    /// Manually constructing objects from key-value pairs using `reduce` or `forEach`
+    /// is more verbose, error-prone, and harder to understand. The `Object.fromEntries`
+    /// method is clearer, more declarative, and built for exactly this purpose.
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// const result = pairs.reduce((obj, [key, value]) => {
+    ///   obj[key] = value;
+    ///   return obj;
+    /// }, {});
+    ///
+    /// const result = {};
+    /// pairs.forEach(([key, value]) => {
+    ///   result[key] = value;
+    /// });
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// const result = Object.fromEntries(pairs);
+    /// ```
+    PreferObjectFromEntries,
+    unicorn,
+    style,
+    pending
+);
+
+impl Rule for PreferObjectFromEntries {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::CallExpression(call_expr) = node.kind() else { return };
+
+        if call_expr.arguments.len() == 1
+            && call_expr.arguments[0].is_expression()
+            && does_expr_match_any_path(
+                &call_expr.callee,
+                self.functions.iter().map(|fun| fun.split('.').collect::<Vec<_>>()),
+            )
+        {
+            ctx.diagnostic(prefer_object_from_entries_diagnostic(call_expr.callee.span()));
+            return;
+        }
+
+        if !is_method_call(call_expr, None, Some(&["reduce"]), Some(2), Some(2)) {
+            return;
+        }
+
+        if call_expr.optional || call_expr.callee.to_member_expression().optional() {
+            return;
+        }
+
+        let Some(accumulator) = call_expr
+            .arguments
+            .get(1)
+            .expect("call expr must have exactly 2 arguments")
+            .as_expression()
+            .map(oxc_ast::ast::Expression::get_inner_expression)
+        else {
+            return;
+        };
+
+        if !is_empty_object(accumulator) {
+            return;
+        }
+
+        let Some(Expression::ArrowFunctionExpression(reducer)) = call_expr
+            .arguments
+            .first()
+            .expect("call expr must have exactly 2 arguments")
+            .as_expression()
+            .map(oxc_ast::ast::Expression::get_inner_expression)
+        else {
+            return;
+        };
+
+        if !reducer.expression || reducer.r#async {
+            return;
+        }
+
+        let Statement::ExpressionStatement(stmt) = reducer
+            .body
+            .statements
+            .first()
+            .expect("arrow function expressions must have at least one body statement")
path: crates/oxc_linter/src/rules/unicorn/prefer_object_from_entries.rs
user: login: graphite-app[bot]
body: There's a typo in the error message: `arrow funciton expressions` should be `arrow function expressions`
```suggestion
            .expect("arrow function expressions must have at least one body statement")
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=oxc-project&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-19T20:12:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -328,3 +328,40 @@ pub fn is_same_member_expression(
         ctx,
     )
 }
+
+pub fn call_expr_member_expr_property_span(call_expr: &CallExpression) -> Span {
+    call_expr
+        .callee
+        .as_member_expression()
+        .and_then(oxc_ast::ast::MemberExpression::static_property_info)
+        .map_or(call_expr.span, |(span, _)| span)
+}
+
+pub fn does_expr_match_any_path<'a, P, S>(mut expr: &Expression, paths: P) -> bool
+where
+    P: IntoIterator<Item = S>,
+    S: AsRef<[&'a str]>,
+{
+    let mut path = Vec::new();
+
+    while let Some(member_expr) = expr.as_member_expression() {
+        let MemberExpression::StaticMemberExpression(static_mem_expr) = member_expr else {
+            return false;
+        };
+        path.push(static_mem_expr.property.name.as_str());
+
+        expr = member_expr.object().get_inner_expression();
+    }
+
+    let Expression::Identifier(ident) = expr else { return false };
+    path.push(ident.name.as_str());
+    let path = path.iter().rev();
+
+    for e in paths {
+        if e.as_ref().iter().zip(path.clone()).all(|(x, y)| x == y) {
+            return true;
+        }
path: crates/oxc_linter/src/utils/unicorn.rs
user: login: graphite-app[bot]
body: The current path matching logic may fail when paths have different lengths, as `zip()` only pairs elements until the shorter iterator is exhausted. Consider either:

1. Checking that path lengths match before comparison:
```rust
if e.as_ref().len() == path.clone().count() && e.as_ref().iter().zip(path.clone()).all(|(x, y)| x == y) {
    return true;
}
```

2. Or using a more robust matching approach that ensures complete path equality rather than just matching the available pairs.
```suggestion

    for e in paths {
        if e.as_ref().len() == path.clone().count() 
            && e.as_ref().iter().zip(path.clone()).all(|(x, y)| x == y) {
            return true;
        }
    }

```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=oxc-project&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-19T20:12:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: camc314
created_at: 2025-05-19T20:08:04Z
body: * **#11174** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11174?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11174?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* **#11170** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11170?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#11169** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11169?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#11168** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11168?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#11167** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11167?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* `main`
<hr />

## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>0-merge</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/oxc-project?ref=merge-queue-instructions-in-stack-comment&prId=11174).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>

<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: CONTRIBUTOR
user: login: codspeed-hq[bot]
created_at: 2025-05-19T20:11:52Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Instrumentation Performance Report](https://codspeed.io/oxc-project/oxc/branches/c%2F05-19-feat_linter_implement_unicorn_prefer-object-from-entries)

### Merging #11174 will **degrade performances by 12.65%**

<sub>Comparing <code>c/05-19-feat_linter_implement_unicorn_prefer-object-from-entries</code> (b3bbdda) with <code>main</code> (20f9458)</sub>



### Summary

`❌ 1` regressions  
`✅ 37` untouched benchmarks  


> :warning: _Please fix the performance issues or [acknowledge them on CodSpeed](https://codspeed.io/oxc-project/oxc/branches/c%2F05-19-feat_linter_implement_unicorn_prefer-object-from-entries)._

### Benchmarks breakdown

|     | Benchmark | `BASE` | `HEAD` | Change |
| --- | --------- | ----------------------- | ------------------- | ------ |
| ❌ | `` mangler[cal.com.tsx] `` | 3 ms | 3.5 ms | -12.65% |

author_association: NONE
user: login: camc314
created_at: 2025-05-19T21:51:15Z
body: ### Merge activity

* **May 19, 5:51 PM EDT**: The merge label '0-merge' was detected. This PR will be added to the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc) once it meets the requirements.
* **May 19, 5:51 PM EDT**: `camc314` added this pull request to the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc).
* **May 19, 9:57 PM UTC**: The [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc) couldn't merge this PR because **it had conflicts with the trunk branch**.
* **May 19, 6:12 PM EDT**: The merge label '0-merge' was detected. This PR will be added to the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc) once it meets the requirements.
* **May 19, 6:17 PM EDT**: `camc314` added this pull request to the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc).
* **May 19, 6:20 PM EDT**: Merged by the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc).

author_association: CONTRIBUTOR