From b3af1c4602a987c72773cc6402278ffc16fdce40 Mon Sep 17 00:00:00 2001 From: Jeremy HERGAULT Date: Fri, 24 Jul 2026 09:41:54 +0200 Subject: [PATCH] feat: update version and support more C++ --- Cargo.toml | 6 +-- src/ast/parse.rs | 111 ++++++++++++++++++++++++++++++++++++++++-- src/transpile/expr.rs | 20 +++++++- src/transpile/item.rs | 6 +-- 4 files changed, 130 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 233b31a..3be4d1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cppshift" -version = "0.1.1" +version = "0.1.2" authors = ["Jérémy HERGAULT", "Enzo PASQUALINI"] description = "CPP parser and transpiler" repository = "https://github.com/worldline/cppshift" @@ -16,11 +16,11 @@ transpiler = ["ast", "dep:serde", "dep:syn", "dep:quote", "dep:proc-macro2"] miette = { version = "7", features = ["fancy"] } thiserror = "2" serde = { version = "1", features = ["derive"], optional = true } -syn = { version = "2", features = ["full", "extra-traits", "printing"], optional = true } +syn = { version = "3", features = ["full", "extra-traits", "printing"], optional = true } quote = { version = "1", optional = true } proc-macro2 = { version = "1", optional = true } [dev-dependencies] -tokio = { version = "1", features = ["macros"] } +tokio = { version = ">=1.11.1, < 2", features = ["macros"] } reqwest = "0.13" glob = "0.3" diff --git a/src/ast/parse.rs b/src/ast/parse.rs index 4c2ed89..331b8fe 100644 --- a/src/ast/parse.rs +++ b/src/ast/parse.rs @@ -638,6 +638,28 @@ fn parse_item_enum<'de>(p: &mut Parser<'de>) -> Result, AstError> } p.expect(TokenKind::RightBrace)?; } + + // A trailing declarator may follow the enum body, e.g. + // `enum { A, B } mField;` declares an unnamed enum type and a member of + // that type in one statement. The declarator (and any initializer) is + // discarded — only the enum type is retained. Skip to the terminating `;`, + // balancing brackets from array declarators or brace/paren initializers. + if p.peek_kind() != Some(TokenKind::Semicolon) { + let mut depth = 0usize; + while let Some(kind) = p.peek_kind() { + match kind { + TokenKind::LeftBrace | TokenKind::LeftParenthese | TokenKind::LeftBracket => { + depth += 1; + } + TokenKind::RightBrace | TokenKind::RightParenthese | TokenKind::RightBracket => { + depth = depth.saturating_sub(1); + } + TokenKind::Semicolon if depth == 0 => break, + _ => {} + } + p.bump()?; + } + } p.expect(TokenKind::Semicolon)?; Ok(ItemEnum { @@ -662,6 +684,9 @@ fn parse_item_struct<'de>(p: &mut Parser<'de>) -> Result, AstErr None }; + // `struct Foo final : ...` — the `final` specifier follows the name. + p.eat(TokenKind::KeywordFinal); + // Forward declaration: struct Foo; if p.eat(TokenKind::Semicolon).is_some() { return Ok(ItemStruct { @@ -705,6 +730,9 @@ fn parse_item_class<'de>(p: &mut Parser<'de>) -> Result, AstError None }; + // `class Foo final : ...` — the `final` specifier follows the name. + p.eat(TokenKind::KeywordFinal); + // Forward declaration: class Foo; if p.eat(TokenKind::Semicolon).is_some() { return Ok(ItemClass { @@ -2154,11 +2182,26 @@ fn parse_fields_named<'de>( } Some(TokenKind::KeywordFriend) => { p.bump()?; - let inner = parse_item(p)?; - members.push(Member::Friend(ItemFriend { - attrs: Vec::new(), - item: Box::new(inner), - })); + // Friend declarations are not emitted, and can carry constructs + // the item parser doesn't handle (e.g. a templated befriended + // class `friend class cFoo;`). Skip to the terminating + // `;`, balancing any `{}`/`()` from an inline friend definition. + let mut brace_depth = 0usize; + let mut paren_depth = 0usize; + while let Some(kind) = p.peek_kind() { + match kind { + TokenKind::LeftBrace => brace_depth += 1, + TokenKind::RightBrace => brace_depth = brace_depth.saturating_sub(1), + TokenKind::LeftParenthese => paren_depth += 1, + TokenKind::RightParenthese => paren_depth = paren_depth.saturating_sub(1), + TokenKind::Semicolon if brace_depth == 0 && paren_depth == 0 => { + p.bump()?; + break; + } + _ => {} + } + p.bump()?; + } continue; } _ => {} @@ -3337,6 +3380,18 @@ fn parse_expr_precedence<'de>( Some(TokenKind::Dot) => { p.bump()?; let member = parse_ident(p)?; + // Explicit template arguments on a method call, e.g. + // `obj.method(...)`. The turbofish is discarded (method calls + // don't retain template args); only recognized when it parses as + // angle-bracketed args and is immediately followed by `(`, so a + // genuine comparison like `a.b < c` is left untouched. + if p.peek_kind() == Some(TokenKind::LeftChevron) { + let cp = p.checkpoint(); + match parse_angle_bracketed_args(p) { + Ok(_) if p.peek_kind() == Some(TokenKind::LeftParenthese) => {} + _ => p.restore(&cp), + } + } if p.peek_kind() == Some(TokenKind::LeftParenthese) { p.bump()?; let mut args = Punctuated::new(); @@ -3368,6 +3423,18 @@ fn parse_expr_precedence<'de>( Some(TokenKind::PointerMember) => { p.bump()?; let member = parse_ident(p)?; + // Explicit template arguments on a method call, e.g. + // `ptr->method(...)`. The turbofish is discarded (method calls + // don't retain template args); only recognized when it parses as + // angle-bracketed args and is immediately followed by `(`, so a + // genuine comparison like `a->b < c` is left untouched. + if p.peek_kind() == Some(TokenKind::LeftChevron) { + let cp = p.checkpoint(); + match parse_angle_bracketed_args(p) { + Ok(_) if p.peek_kind() == Some(TokenKind::LeftParenthese) => {} + _ => p.restore(&cp), + } + } if p.peek_kind() == Some(TokenKind::LeftParenthese) { p.bump()?; let mut args = Punctuated::new(); @@ -4336,6 +4403,40 @@ mod tests { } } + #[test] + fn parse_enum_with_trailing_declarator() { + // Anonymous enum declaring a member in one statement (C++ allows this). + let file = parse("enum { FC_NONE, FC_ALL } mFuncCallHistoryType;"); + match &file.items[0] { + Item::Enum(e) => { + assert!(e.ident.is_none()); + assert_eq!(e.variants.len(), 2); + } + other => panic!("expected Enum, got {other:?}"), + } + } + + #[test] + fn parse_method_call_with_template_args() { + // Explicit template arguments on a method call (turbofish); the args are + // discarded but the call must parse as a method call. + let file = parse("void f() { pTxn->searchDecoW(); }"); + match &file.items[0] { + Item::Fn(_) => {} + other => panic!("expected Fn, got {other:?}"), + } + } + + #[test] + fn parse_arrow_comparison_still_works() { + // A genuine comparison after `->` must not be mistaken for a turbofish. + let file = parse("void f() { bool b = a->x < c; }"); + match &file.items[0] { + Item::Fn(_) => {} + other => panic!("expected Fn, got {other:?}"), + } + } + #[test] fn parse_struct_forward() { let file = parse("struct Foo;"); diff --git a/src/transpile/expr.rs b/src/transpile/expr.rs index 3b4795d..bc81c24 100644 --- a/src/transpile/expr.rs +++ b/src/transpile/expr.rs @@ -4,7 +4,7 @@ use quote::ToTokens; use crate::SourceCodeSpan as _; use crate::ast::expr::{ BinaryOp, Expr, ExprBinary, ExprBool, ExprIndex, ExprMethodCall, ExprNullptr, ExprParen, - ExprUnary, UnaryOp, + ExprUnary, LitKind, UnaryOp, }; use crate::transpile::{Transpile, TranspileContext, Transpiler}; @@ -90,7 +90,23 @@ impl<'de> Transpile for Expr<'de> { ) -> Result<(), TranspileError> { match self { Expr::Lit(lit) => { - let rust_expr: syn::Expr = syn::parse_str(lit.span.src()).map_err(|e| { + // C++ integer literals may carry width/sign suffixes (`LL`, `ULL`, + // `UL`, `L`, `U`) that are not valid Rust; strip them so the + // literal parses. Only applied to integer literals, so string and + // char literals (which may legitimately contain those letters) are + // untouched. + let src = lit.span.src(); + let normalized = if lit.kind == LitKind::Integer { + src.trim_end_matches(['l', 'L', 'u', 'U']) + } else { + src + }; + let normalized = if normalized.is_empty() { + src + } else { + normalized + }; + let rust_expr: syn::Expr = syn::parse_str(normalized).map_err(|e| { unsupported_from_expr(&format!("cannot parse literal: {e}"), self) })?; tokens.extend(quote::quote!(#rust_expr)); diff --git a/src/transpile/item.rs b/src/transpile/item.rs index 69fcb87..41bb765 100644 --- a/src/transpile/item.rs +++ b/src/transpile/item.rs @@ -379,7 +379,7 @@ mod tests { } else { panic!( "expected field member[0], got {:?}", - &named_fields.members[0] + named_fields.members[0] ); } @@ -394,7 +394,7 @@ mod tests { } else { panic!( "expected field member[3], got {:?}", - &named_fields.members[3] + named_fields.members[3] ); } @@ -409,7 +409,7 @@ mod tests { } else { panic!( "expected field member[5], got {:?}", - &named_fields.members[5] + named_fields.members[5] ); } }