Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions .changeset/unique-formal-parameters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
swc_core: patch
swc_ecma_parser: patch
---

fix(es/parser): enforce UniqueFormalParameters for methods, arrows, and non-simple forms
8 changes: 8 additions & 0 deletions crates/swc_ecma_parser/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ pub enum SyntaxError {
DuplicatedRegExpFlags(char),
UnknownRegExpFlags,

/// Duplicate binding in UniqueFormalParameters / non-simple
/// FormalParameters.
DuplicateFormalParameter(Atom),

TS1003,
TS1005,
TS1009,
Expand Down Expand Up @@ -592,6 +596,10 @@ impl SyntaxError {
}
SyntaxError::UnknownRegExpFlags => "Unknown regular expression flags.".into(),

SyntaxError::DuplicateFormalParameter(name) => {
format!("Duplicate parameter name not allowed in this context: {name}").into()
}

SyntaxError::TS1003 => "Expected an identifier".into(),
SyntaxError::TS1005 => "Expected a semicolon".into(),
SyntaxError::TS1009 => "Trailing comma is not allowed".into(),
Expand Down
12 changes: 10 additions & 2 deletions crates/swc_ecma_parser/src/parser/class_and_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,14 @@ impl<I: Tokens> Parser<I> {
params.is_simple_parameter_list(),
)?;

// `function f(a, a) { "use strict"; }` is an early error even though
// the parameter list is simple and the outer context may be sloppy.
if let Some(body) = &body {
if params.is_simple_parameter_list() && has_use_strict(body).is_some() {
p.ensure_unique_formal_params(params.iter().map(|param| &param.pat));
}
}

if p.syntax().flow() && body.is_none() && !p.ctx().contains(Context::InDeclare) {
p.emit_err(p.input().cur_span(), SyntaxError::TS1005);
}
Expand Down Expand Up @@ -1293,7 +1301,7 @@ impl<I: Tokens> Parser<I> {
}));
} else {
return self.make_method(
Self::parse_formal_params,
Self::parse_unique_formal_params,
MakeMethodArgs {
start,
is_optional,
Expand Down Expand Up @@ -1448,7 +1456,7 @@ impl<I: Tokens> Parser<I> {
),
Token::Set => self.make_method(
|p| {
let params = p.parse_formal_params()?;
let params = p.parse_unique_formal_params()?;

if p.syntax().flow() && params.iter().any(|p| !is_not_this(p)) {
p.emit_err(key_span, SyntaxError::TS1003);
Expand Down
2 changes: 2 additions & 0 deletions crates/swc_ecma_parser/src/parser/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2716,6 +2716,8 @@ impl<I: Tokens> Parser<I> {
};

let validate_arrow_params = |p: &mut Self, params: &[Pat], is_async: bool| {
p.ensure_unique_formal_params(params.iter());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply arrow uniqueness checks to all TS arrow paths

Please route the other TypeScript arrow parsers through this new uniqueness check as well. This closure only runs in the main parenthesized-arrow path, but parse_paren_expr_or_arrow_fn returns earlier for the conditional-expression typed-arrow slow path, and try_parse_ts_generic_async_arrow_fn parses generic async arrows separately; in sloppy TS both still accept simple duplicates such as cond ? (a, a): number => a : 0 and async <T>(a, a) => a, so the new UniqueFormalParameters enforcement remains incomplete for arrows.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run generic-arrow duplicate checks after speculative TS parse

For non-async TypeScript generic arrows such as <T>(a, a) => a, this validation runs while parse_assignment_expr_base is being called from try_parse_ts, which sets Context::IgnoreError; emit_err therefore discards the duplicate-parameter diagnostic even though the speculative parse succeeds and returns the arrow. That leaves generic arrows with duplicate parameters accepted, so the uniqueness check needs to happen after the speculative parse commits or otherwise outside the ignored-error context.

Useful? React with 👍 / 👎.


for param in params {
if is_async {
match param {
Expand Down
2 changes: 1 addition & 1 deletion crates/swc_ecma_parser/src/parser/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ impl<I: Tokens> Parser<I> {
Vec::new(),
start,
|p| {
let params = p.parse_formal_params()?;
let params = p.parse_unique_formal_params()?;

if params.iter().filter(|p| is_not_this(p)).count() != 1 {
p.emit_err(key_span, SyntaxError::SetterParam);
Expand Down
115 changes: 112 additions & 3 deletions crates/swc_ecma_parser/src/parser/pat.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
//! 13.3.3 Destructuring Binding Patterns

use rustc_hash::FxHashSet;
use swc_atoms::Atom;
use swc_common::Spanned;

use super::*;
use crate::parser::{expr::AssignTargetOrSpread, Parser};
use crate::parser::{expr::AssignTargetOrSpread, util::IsSimpleParameterList, Parser};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PatType {
Expand Down Expand Up @@ -77,6 +79,63 @@ impl<I: Tokens> Parser<I> {
}
}

/// Enforce UniqueFormalParameters: BoundNames must not contain duplicates.
///
/// Used for methods, setters, arrows, constructors, strict-mode functions,
/// and any FormalParameters list that is not a simple parameter list.
pub(crate) fn ensure_unique_formal_params<'a, Iter>(&mut self, pats: Iter)
where
Iter: IntoIterator<Item = &'a Pat>,
{
let mut names = FxHashSet::default();
for pat in pats {
self.collect_unique_formal_bindings(pat, &mut names);
}
}

fn collect_unique_formal_bindings(&mut self, pat: &Pat, names: &mut FxHashSet<Atom>) {
match pat {
Pat::Ident(i) => {
if !names.insert(i.id.sym.clone()) {
self.emit_err(
i.id.span,
SyntaxError::DuplicateFormalParameter(i.id.sym.clone()),
);
}
}
Pat::Array(arr) => {
for elem in arr.elems.iter().flatten() {
self.collect_unique_formal_bindings(elem, names);
}
}
Pat::Rest(r) => self.collect_unique_formal_bindings(&r.arg, names),
Pat::Object(obj) => {
for prop in &obj.props {
match prop {
ObjectPatProp::KeyValue(KeyValuePatProp { value, .. })
| ObjectPatProp::Rest(RestPat { arg: value, .. }) => {
self.collect_unique_formal_bindings(value, names);
}
ObjectPatProp::Assign(AssignPatProp { key, .. }) => {
if !names.insert(key.sym.clone()) {
self.emit_err(
key.span,
SyntaxError::DuplicateFormalParameter(key.sym.clone()),
);
}
}
#[cfg(swc_ast_unknown)]
_ => unreachable!(),
}
}
}
Pat::Assign(a) => self.collect_unique_formal_bindings(&a.left, names),
Pat::Invalid(_) | Pat::Expr(_) => {}
#[cfg(swc_ast_unknown)]
_ => unreachable!(),
}
}

fn assign_pat_type_ann(&mut self, pat: &mut Pat, span: Span, type_ann: Box<TsType>) {
let type_ann = Some(Box::new(TsTypeAnn { span, type_ann }));

Expand Down Expand Up @@ -767,6 +826,36 @@ impl<I: Tokens> Parser<I> {
}
}

// Class constructors always use UniqueFormalParameters (class bodies are
// strict mode code).
{
let mut names = FxHashSet::default();
for p in &params {
match p {
ParamOrTsParamProp::Param(param) => {
self.collect_unique_formal_bindings(&param.pat, &mut names);
}
ParamOrTsParamProp::TsParamProp(prop) => match &prop.param {
TsParamPropParam::Ident(i) => {
if !names.insert(i.id.sym.clone()) {
self.emit_err(
i.id.span,
SyntaxError::DuplicateFormalParameter(i.id.sym.clone()),
);
}
}
TsParamPropParam::Assign(a) => {
self.collect_unique_formal_bindings(&a.left, &mut names);
}
#[cfg(swc_ast_unknown)]
_ => {}
},
#[cfg(swc_ast_unknown)]
_ => {}
}
}
}

Ok(params)
}

Expand Down Expand Up @@ -867,12 +956,32 @@ impl<I: Tokens> Parser<I> {
}
}

// UniqueFormalParameters / FormalParameters early errors:
// duplicate BoundNames are always illegal in strict mode, and also
// illegal whenever the parameter list is not simple.
if self.ctx().contains(Context::Strict) || !params.is_simple_parameter_list() {
self.ensure_unique_formal_params(params.iter().map(|p| &p.pat));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Buffer strict duplicate checks for auto-detected modules

Please don't make this depend only on the current Context::Strict for simple parameter lists. parse_program parses the file before it knows whether a later import/export will turn it into a module, and existing strict-mode checks use the module-error buffer for that reason; with this condition, function f(a, a) {} export {}; is parsed while still non-strict, so no duplicate is recorded before module mode is enabled. The same applies to module parser entry points that set Context::Module without Context::Strict, so module code can still accept simple duplicate function parameters.

Useful? React with 👍 / 👎.

}

if self.ctx().contains(Context::Strict) {
for param in params.iter() {
self.pat_is_valid_argument_in_strict(&param.pat)
}
}

Ok(params)
}

pub(crate) fn parse_unique_formal_params(&mut self) -> PResult<Vec<Param>> {
// FIXME: This is wrong
self.parse_formal_params()
let params = self.parse_formal_params()?;
// Methods / object methods always require UniqueFormalParameters,
// including simple duplicate bindings that sloppy-mode functions allow.
// `parse_formal_params` already checks when !simple or Strict; cover the
// remaining simple+non-strict case used by class/object methods.
if params.is_simple_parameter_list() && !self.ctx().contains(Context::Strict) {
self.ensure_unique_formal_params(params.iter().map(|p| &p.pat));
}
Ok(params)
}

pub(super) fn parse_paren_items_as_params(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(a, a) => a;
async (a, a) => a;
({a}, a) => a;
(a, a = 1) => a;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/arrow/input.js:1:1]
1 | (a, a) => a;
: ^
2 | async (a, a) => a;
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/arrow/input.js:2:1]
1 | (a, a) => a;
2 | async (a, a) => a;
: ^
3 | ({a}, a) => a;
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/arrow/input.js:3:1]
2 | async (a, a) => a;
3 | ({a}, a) => a;
: ^
4 | (a, a = 1) => a;
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/arrow/input.js:4:1]
3 | ({a}, a) => a;
4 | (a, a = 1) => a;
: ^
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
({ async m(a, a) { return a; } });
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/async-method/input.js:1:1]
1 | ({ async m(a, a) { return a; } });
: ^
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class C {
constructor(a, a) {}
}
class D {
constructor([a], a) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/class-constructor/input.js:2:1]
1 | class C {
2 | constructor(a, a) {}
: ^
3 | }
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/class-constructor/input.js:5:1]
4 | class D {
5 | constructor([a], a) {}
: ^
6 | }
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class C {
m(a, a) { return a; }
static m2(a, a) { return a; }
async m3(a, a) { return a; }
*m4(a, a) { return a; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/class-method/input.js:2:1]
1 | class C {
2 | m(a, a) { return a; }
: ^
3 | static m2(a, a) { return a; }
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/class-method/input.js:3:1]
2 | m(a, a) { return a; }
3 | static m2(a, a) { return a; }
: ^
4 | async m3(a, a) { return a; }
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/class-method/input.js:4:1]
3 | static m2(a, a) { return a; }
4 | async m3(a, a) { return a; }
: ^
5 | *m4(a, a) { return a; }
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/class-method/input.js:5:1]
4 | async m3(a, a) { return a; }
5 | *m4(a, a) { return a; }
: ^
6 | }
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
({ *m(a, a) { return a; } });
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/generator-method/input.js:1:1]
1 | ({ *m(a, a) { return a; } });
: ^
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
function f([a], a) {}
function g({a}, a) {}
function h(a, a = 1) {}
function i(a = 1, a) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/non-simple-fn/input.js:1:1]
1 | function f([a], a) {}
: ^
2 | function g({a}, a) {}
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/non-simple-fn/input.js:2:1]
1 | function f([a], a) {}
2 | function g({a}, a) {}
: ^
3 | function h(a, a = 1) {}
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/non-simple-fn/input.js:3:1]
2 | function g({a}, a) {}
3 | function h(a, a = 1) {}
: ^
4 | function i(a = 1, a) {}
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/non-simple-fn/input.js:4:1]
3 | function h(a, a = 1) {}
4 | function i(a = 1, a) {}
: ^
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
({ m(a, a) { return a; } });
({ m([a], a) { return a; } });
({ m({a}, a) { return a; } });
({ m(a, a = 1) { return a; } });
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/object-method/input.js:1:1]
1 | ({ m(a, a) { return a; } });
: ^
2 | ({ m([a], a) { return a; } });
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/object-method/input.js:2:1]
1 | ({ m(a, a) { return a; } });
2 | ({ m([a], a) { return a; } });
: ^
3 | ({ m({a}, a) { return a; } });
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/object-method/input.js:3:1]
2 | ({ m([a], a) { return a; } });
3 | ({ m({a}, a) { return a; } });
: ^
4 | ({ m(a, a = 1) { return a; } });
`----
x Duplicate parameter name not allowed in this context: a
,-[$DIR/tests/errors/unique-formal-params/object-method/input.js:4:1]
3 | ({ m({a}, a) { return a; } });
4 | ({ m(a, a = 1) { return a; } });
: ^
`----
Loading
Loading