Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 76 additions & 6 deletions crates/oxc_linter/src/rules/unicorn/no_new_buffer.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use oxc_ast::{AstKind, ast::Expression};
use oxc_ast::{
AstKind,
ast::{Argument, Expression},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use oxc_semantic::IsGlobalReference;
use oxc_span::{GetSpan, Span};

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

Expand Down Expand Up @@ -37,7 +41,7 @@ declare_oxc_lint!(
NoNewBuffer,
unicorn,
pedantic,
pending
suggestion
);

impl Rule for NoNewBuffer {
Expand All @@ -49,10 +53,54 @@ impl Rule for NoNewBuffer {
let Expression::Identifier(ident) = &new_expr.callee.without_parentheses() else {
return;
};
if ident.name != "Buffer" {
if ident.name != "Buffer" || !ident.is_global_reference(ctx.scoping()) {
return;
}
ctx.diagnostic(no_new_buffer_diagnostic(ident.span));

// Determine which method to use based on argument type
let method = determine_buffer_method(new_expr);
let expr_span = new_expr.span;

ctx.diagnostic_with_suggestion(no_new_buffer_diagnostic(ident.span), |fixer| {
let Some(method) = method else {
return fixer.noop();
};

// Build arguments string
let args_text = new_expr
.arguments
.iter()
.map(|arg| ctx.source_range(arg.span()))
.collect::<Vec<_>>()
.join(", ");

let replacement = format!("Buffer.{method}({args_text})");
fixer.replace(expr_span, replacement)
});
}
}

/// Determines which Buffer method to use based on the first argument.
/// Returns `Some("alloc")` for numeric arguments, `Some("from")` for array/string arguments,
/// or `None` if the type can't be determined (unsafe to fix).
fn determine_buffer_method(new_expr: &oxc_ast::ast::NewExpression) -> Option<&'static str> {
// Handle spread arguments - unsafe to fix
if new_expr.arguments.iter().any(Argument::is_spread) {
return None;
}

let first_arg = new_expr.arguments.first()?.as_expression()?;
let first_arg = first_arg.without_parentheses();

match first_arg {
// Numeric literals → Buffer.alloc
Expression::NumericLiteral(_) => Some("alloc"),
// String/template literals → Buffer.from
Expression::StringLiteral(_)
| Expression::TemplateLiteral(_)
| Expression::ArrayExpression(_) => Some("from"),
// For other expressions, we can't safely determine the type
_ => None,
}
}

Expand All @@ -67,6 +115,7 @@ fn test() {
r"const buffer = Buffer.from('7468697320697320612074c3a97374', 'hex')",
r"const buffer = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72])",
r"const buffer = Buffer.alloc(10)",
r"const Buffer = function () {}; new Buffer(10);",
];

let fail = vec![
Expand All @@ -86,5 +135,26 @@ fn test() {
r"new Buffer(input, encoding);",
];

Tester::new(NoNewBuffer::NAME, NoNewBuffer::PLUGIN, pass, fail).test_and_snapshot();
let fix = vec![
// Numeric argument → Buffer.alloc
(r"const buffer = new Buffer(10);", r"const buffer = Buffer.alloc(10);"),
// String argument → Buffer.from
(r#"const buffer = new Buffer("string");"#, r#"const buffer = Buffer.from("string");"#),
(
r#"const buffer = new Buffer("7468697320697320612074c3a97374", "hex")"#,
r#"const buffer = Buffer.from("7468697320697320612074c3a97374", "hex")"#,
),
// Array argument → Buffer.from
(
r"const buffer = new Buffer([0x62, 0x75, 0x66, 0x66, 0x65, 0x72])",
r"const buffer = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72])",
),
(r"const buffer = new Buffer([0x62, bar])", r"const buffer = Buffer.from([0x62, bar])"),
// Template literal → Buffer.from
(r"const buffer = new Buffer(`${unknown}`)", r"const buffer = Buffer.from(`${unknown}`)"),
];

Tester::new(NoNewBuffer::NAME, NoNewBuffer::PLUGIN, pass, fail)
.expect_fix(fix)
.test_and_snapshot();
}
Loading