Skip to content

Commit 62d922b

Browse files
ssrliveCopilot
andcommitted
fix: clusters 7,25 — constructor special method, super heritage, private names
- Reject generator/async/async-generator constructor methods as SyntaxError - Reject super() in constructor without class heritage (extends) - Track class heritage via thread-local for nested class support - Fix private name ZWJ/ZWNJ validation (U+200D/U+200C not valid as ID_Start after #) - Skip computed property brackets in private name pre-scan - Push method context for field initializers (super property access in arrows) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c6c6a5d commit 62d922b

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

src/core/parser.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ thread_local! {
193193
/// super() is allowed when this is >0.
194194
static CONSTRUCTOR_CONTEXT: RefCell<usize> = const { RefCell::new(0) };
195195
static CONSTRUCTOR_CONTEXT_STACK: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
196+
/// Whether the current class being parsed has an extends clause (heritage).
197+
/// Stacked to support nested classes.
198+
static CLASS_HAS_HERITAGE: RefCell<bool> = const { RefCell::new(false) };
199+
static CLASS_HAS_HERITAGE_STACK: RefCell<Vec<bool>> = const { RefCell::new(Vec::new()) };
196200
}
197201
fn forbid_in() -> bool {
198202
FORBID_IN.with(|c| *c.borrow() > 0)
@@ -240,6 +244,20 @@ fn push_constructor_context() {
240244
fn pop_constructor_context() {
241245
CONSTRUCTOR_CONTEXT.with(|c| *c.borrow_mut() -= 1);
242246
}
247+
fn class_has_heritage() -> bool {
248+
CLASS_HAS_HERITAGE.with(|c| *c.borrow())
249+
}
250+
fn push_class_heritage(has: bool) {
251+
CLASS_HAS_HERITAGE_STACK.with(|s| {
252+
s.borrow_mut().push(CLASS_HAS_HERITAGE.with(|c| *c.borrow()));
253+
});
254+
CLASS_HAS_HERITAGE.with(|c| *c.borrow_mut() = has);
255+
}
256+
fn pop_class_heritage() {
257+
if let Some(prev) = CLASS_HAS_HERITAGE_STACK.with(|s| s.borrow_mut().pop()) {
258+
CLASS_HAS_HERITAGE.with(|c| *c.borrow_mut() = prev);
259+
}
260+
}
243261
fn in_function_context() -> bool {
244262
FUNCTION_CONTEXT.with(|c| *c.borrow() > 0)
245263
}
@@ -488,7 +506,9 @@ fn parse_class_declaration(t: &[TokenData], index: &mut usize) -> Result<Stateme
488506
} else {
489507
None
490508
};
509+
push_class_heritage(extends.is_some());
491510
let members = parse_class_body(t, index)?;
511+
pop_class_heritage();
492512
let class_def = crate::core::ClassDefinition { name, extends, members };
493513
Ok(Statement {
494514
kind: Box::new(StatementKind::Class(Box::new(class_def))),
@@ -3822,6 +3842,20 @@ pub fn parse_class_body(t: &[TokenData], index: &mut usize) -> Result<Vec<ClassM
38223842
pos += 1;
38233843
continue;
38243844
}
3845+
// Skip computed property brackets — names inside are usages, not declarations
3846+
if matches!(t[pos].token, Token::LBracket) {
3847+
let mut depth = 1usize;
3848+
pos += 1;
3849+
while pos < t.len() && depth > 0 {
3850+
if matches!(t[pos].token, Token::LBracket) {
3851+
depth += 1;
3852+
} else if matches!(t[pos].token, Token::RBracket) {
3853+
depth -= 1;
3854+
}
3855+
pos += 1;
3856+
}
3857+
continue;
3858+
}
38253859
if matches!(t[pos].token, Token::Static) {
38263860
pos += 1;
38273861
if pos < t.len() && matches!(t[pos].token, Token::LBrace) {
@@ -4208,6 +4242,17 @@ pub fn parse_class_body(t: &[TokenData], index: &mut usize) -> Result<Vec<ClassM
42084242
}
42094243
*index += 1;
42104244
}
4245+
// It is a Syntax Error if PropName of MethodDefinition is "constructor" and SpecialMethod is true.
4246+
if computed_key_expr.is_none()
4247+
&& !is_static
4248+
&& !is_private
4249+
&& name_str_opt.as_deref() == Some("constructor")
4250+
&& (is_generator || is_async_member)
4251+
{
4252+
return Err(raise_parse_error!(
4253+
"SyntaxError: Class constructor may not be an async method or a generator"
4254+
));
4255+
}
42114256
if computed_key_expr.is_none()
42124257
&& !is_static
42134258
&& !is_private
@@ -4340,7 +4385,11 @@ pub fn parse_class_body(t: &[TokenData], index: &mut usize) -> Result<Vec<ClassM
43404385
}
43414386
} else if *index < t.len() && matches!(t[*index].token, Token::Assign) {
43424387
*index += 1;
4388+
// Field initializers have an implicit [[HomeObject]], so super property
4389+
// access is valid inside arrow functions in field initializers.
4390+
push_method_context();
43434391
let value = parse_expression(t, index)?;
4392+
pop_method_context();
43444393
if *index < t.len() {
43454394
match t[*index].token {
43464395
Token::Semicolon | Token::LineTerminator => *index += 1,
@@ -4549,7 +4598,9 @@ fn parse_primary(tokens: &[TokenData], index: &mut usize, allow_call: bool) -> R
45494598
} else {
45504599
None
45514600
};
4601+
push_class_heritage(extends.is_some());
45524602
let members = parse_class_body(tokens, index)?;
4603+
pop_class_heritage();
45534604
let class_def = crate::core::ClassDefinition { name, extends, members };
45544605
Expr::Class(Box::new(class_def))
45554606
}
@@ -4798,6 +4849,12 @@ fn parse_primary(tokens: &[TokenData], index: &mut usize, allow_call: bool) -> R
47984849
"'super()' is only valid inside a class constructor"
47994850
));
48004851
}
4852+
if !class_has_heritage() {
4853+
return Err(raise_parse_error_with_token!(
4854+
tokens[*index - 1],
4855+
"'super()' is only valid in a derived class constructor"
4856+
));
4857+
}
48014858
*index += 1;
48024859
let mut args = Vec::new();
48034860
if *index < tokens.len() && !matches!(tokens[*index].token, Token::RParen) {

src/core/token.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1668,7 +1668,8 @@ pub fn tokenize(expr: &str) -> Result<Vec<TokenData>, JSError> {
16681668
match u32::from_str_radix(&hex, 16).ok().and_then(std::char::from_u32) {
16691669
Some(ch) => {
16701670
// Validate decoded char is valid in identifier
1671-
if ident.is_empty() {
1671+
// For private names (#foo), the char after # must be ID_Start
1672+
if ident.is_empty() || ident == "#" {
16721673
if !(is_id_start(ch) || other_id_start_contains(ch) || ch == '_' || ch == '$') {
16731674
return Err(raise_tokenize_error!(
16741675
"Invalid character from unicode escape in identifier",
@@ -1709,7 +1710,8 @@ pub fn tokenize(expr: &str) -> Result<Vec<TokenData>, JSError> {
17091710
match u32::from_str_radix(&hex, 16).ok().and_then(std::char::from_u32) {
17101711
Some(ch) => {
17111712
// Validate decoded char is valid in identifier
1712-
if ident.is_empty() {
1713+
// For private names (#foo), the char after # must be ID_Start
1714+
if ident.is_empty() || ident == "#" {
17131715
if !(is_id_start(ch) || other_id_start_contains(ch) || ch == '_' || ch == '$') {
17141716
return Err(raise_tokenize_error!(
17151717
"Invalid character from unicode escape in identifier",

0 commit comments

Comments
 (0)