| // Part of the Crubit project, under the Apache License v2.0 with LLVM | |
| // Exceptions. See /LICENSE for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| use arc_anyhow::{anyhow, ensure, Result}; | |
| use heck::ToSnakeCase; | |
| use proc_macro2::{Ident, TokenStream, TokenTree}; | |
| use quote::{format_ident, quote, ToTokens}; | |
| use std::borrow::Cow; | |
| use std::collections::BTreeSet; | |
| use std::fmt::Write as _; | |
| use std::rc::Rc; | |
| use crubit_feature::CrubitFeature; | |
| use dyn_format::Format; | |
| use flagset::FlagSet; | |
| #[derive(Copy, Clone, Debug, Eq, PartialEq)] | |
| pub enum CcConstQualifier { | |
| Mut, | |
| Const, | |
| } | |
| /// Returns Some(..) if the cpp_type is a C++ pointer type. | |
| pub fn is_cpp_pointer_type(cpp_type: TokenStream) -> Option<CcConstQualifier> { | |
| let mut first = None; | |
| let mut last = None; | |
| let mut prev_last = None; | |
| for token in cpp_type.into_iter() { | |
| first = if first.is_none() { Some(token.clone()) } else { first }; | |
| prev_last = last; | |
| last = Some(token); | |
| } | |
| match (first, prev_last, last) { | |
| (Some(TokenTree::Ident(first)), _, Some(TokenTree::Punct(last))) | |
| if first == "const" && last.as_char() == '*' => | |
| { | |
| Some(CcConstQualifier::Const) | |
| } | |
| (_, Some(TokenTree::Ident(prev_last)), Some(TokenTree::Punct(last))) | |
| if prev_last == "const" && last.as_char() == '*' => | |
| { | |
| Some(CcConstQualifier::Const) | |
| } | |
| (_, _, Some(TokenTree::Punct(last))) if last.as_char() == '*' => { | |
| Some(CcConstQualifier::Mut) | |
| } | |
| _ => None, | |
| } | |
| } | |
| /// Returns true if the given identifier is a C++ reserved keyword according to | |
| /// https://en.cppreference.com/w/cpp/keyword | |
| pub fn is_cpp_reserved_keyword(ident: &str) -> bool { | |
| static RESERVED_CC_KEYWORDS: phf::Set<&'static str> = phf::phf_set! { | |
| "alignas", | |
| "alignof", | |
| "and", | |
| "and_eq", | |
| "asm", | |
| "atomic_cancel", | |
| "atomic_commit", | |
| "atomic_noexcept", | |
| "auto", | |
| "bitand", | |
| "bitor", | |
| "bool", | |
| "break", | |
| "case", | |
| "catch", | |
| "char", | |
| "char8_t", | |
| "char16_t", | |
| "char32_t", | |
| "class", | |
| "compl", | |
| "concept", | |
| "const", | |
| "consteval", | |
| "constexpr", | |
| "constinit", | |
| "const_cast", | |
| "continue", | |
| "co_await", | |
| "co_return", | |
| "co_yield", | |
| "decltype", | |
| "default", | |
| "delete", | |
| "do", | |
| "double", | |
| "dynamic_cast", | |
| "else", | |
| "enum", | |
| "explicit", | |
| "export", | |
| "extern", | |
| "false", | |
| "float", | |
| "for", | |
| "friend", | |
| "goto", | |
| "if", | |
| "inline", | |
| "int", | |
| "long", | |
| "mutable", | |
| "namespace", | |
| "new", | |
| "noexcept", | |
| "not", | |
| "not_eq", | |
| "nullptr", | |
| "operator", | |
| "or", | |
| "or_eq", | |
| "private", | |
| "protected", | |
| "public", | |
| "reflexpr", | |
| "register", | |
| "reinterpret_cast", | |
| "requires", | |
| "return", | |
| "short", | |
| "signed", | |
| "sizeof", | |
| "static", | |
| "static_assert", | |
| "static_cast", | |
| "struct", | |
| "switch", | |
| "synchronized", | |
| "template", | |
| "this", | |
| "thread_local", | |
| "throw", | |
| "true", | |
| "try", | |
| "typedef", | |
| "typeid", | |
| "typename", | |
| "union", | |
| "unsigned", | |
| "using", | |
| "virtual", | |
| "void", | |
| "volatile", | |
| "wchar_t", | |
| "while", | |
| "xor", | |
| "xor_eq", | |
| }; | |
| RESERVED_CC_KEYWORDS.contains(ident) | |
| } | |
| /// Returns true if the given identifier is reserved in C++, either as a keyword | |
| /// or as a standard macro that should not be shadowed. | |
| pub fn is_cpp_nonportable_word(ident: &str, features: FlagSet<CrubitFeature>) -> bool { | |
| if is_cpp_reserved_keyword(ident) { | |
| return true; | |
| } | |
| if !features.contains(CrubitFeature::ReserveStandardMacros) { | |
| return false; | |
| } | |
| // Keep in sync with https://en.cppreference.com/cpp/symbol_index/macro | |
| static RESERVED_CC_MACROS: phf::Set<&'static str> = phf::phf_set! { | |
| "__alignas_is_defined", | |
| "__bool_true_false_are_defined", | |
| "__STDC_ENDIAN_BIG__", | |
| "__STDC_ENDIAN_LITTLE__", | |
| "__STDC_ENDIAN_NATIVE__", | |
| "__STDC_VERSION_STDBIT_H__", | |
| "__STDC_VERSION_STDCKDINT_H__", | |
| "_Atomic", | |
| "_IOFBF", | |
| "_IOLBF", | |
| "_IONBF", | |
| "assert", | |
| "ATOMIC_BOOL_LOCK_FREE", | |
| "ATOMIC_CHAR_LOCK_FREE", | |
| "ATOMIC_CHAR16_T_LOCK_FREE", | |
| "ATOMIC_CHAR32_T_LOCK_FREE", | |
| "ATOMIC_CHAR8_T_LOCK_FREE", | |
| "ATOMIC_FLAG_INIT", | |
| "ATOMIC_INT_LOCK_FREE", | |
| "ATOMIC_LLONG_LOCK_FREE", | |
| "ATOMIC_LONG_LOCK_FREE", | |
| "ATOMIC_POINTER_LOCK_FREE", | |
| "ATOMIC_SHORT_LOCK_FREE", | |
| "ATOMIC_VAR_INIT", | |
| "ATOMIC_WCHAR_T_LOCK_FREE", | |
| "BOOL_WIDTH", | |
| "BUFSIZ", | |
| "CHAR_BIT", | |
| "CHAR_MAX", | |
| "CHAR_MIN", | |
| "CHAR_WIDTH", | |
| "CLOCKS_PER_SEC", | |
| "DBL_DECIMAL_DIG", | |
| "DBL_DIG", | |
| "DBL_EPSILON", | |
| "DBL_HAS_SUBNORM", | |
| "DBL_MANT_DIG", | |
| "DBL_MAX", | |
| "DBL_MAX_10_EXP", | |
| "DBL_MAX_EXP", | |
| "DBL_MIN", | |
| "DBL_MIN_10_EXP", | |
| "DBL_MIN_EXP", | |
| "DBL_TRUE_MIN", | |
| "DECIMAL_DIG", | |
| "E2BIG", | |
| "EACCES", | |
| "EADDRINUSE", | |
| "EADDRNOTAVAIL", | |
| "EAFNOSUPPORT", | |
| "EAGAIN", | |
| "EALREADY", | |
| "EBADF", | |
| "EBADMSG", | |
| "EBUSY", | |
| "ECANCELED", | |
| "ECHILD", | |
| "ECONNABORTED", | |
| "ECONNREFUSED", | |
| "ECONNRESET", | |
| "EDEADLK", | |
| "EDESTADDRREQ", | |
| "EDOM", | |
| "EEXIST", | |
| "EFAULT", | |
| "EFBIG", | |
| "EHOSTUNREACH", | |
| "EIDRM", | |
| "EILSEQ", | |
| "EINPROGRESS", | |
| "EINTR", | |
| "EINVAL", | |
| "EIO", | |
| "EISCONN", | |
| "EISDIR", | |
| "ELOOP", | |
| "EMFILE", | |
| "EMLINK", | |
| "EMSGSIZE", | |
| "ENAMETOOLONG", | |
| "ENETDOWN", | |
| "ENETRESET", | |
| "ENETUNREACH", | |
| "ENFILE", | |
| "ENOBUFS", | |
| "ENODATA", | |
| "ENODEV", | |
| "ENOENT", | |
| "ENOEXEC", | |
| "ENOLCK", | |
| "ENOLINK", | |
| "ENOMEM", | |
| "ENOMSG", | |
| "ENOPROTOOPT", | |
| "ENOSPC", | |
| "ENOSR", | |
| "ENOSTR", | |
| "ENOSYS", | |
| "ENOTCONN", | |
| "ENOTDIR", | |
| "ENOTEMPTY", | |
| "ENOTRECOVERABLE", | |
| "ENOTSOCK", | |
| "ENOTSUP", | |
| "ENOTTY", | |
| "ENXIO", | |
| "EOF", | |
| "EOPNOTSUPP", | |
| "EOVERFLOW", | |
| "EOWNERDEAD", | |
| "EPERM", | |
| "EPIPE", | |
| "EPROTO", | |
| "EPROTONOSUPPORT", | |
| "EPROTOTYPE", | |
| "ERANGE", | |
| "EROFS", | |
| "errno", | |
| "ESPIPE", | |
| "ESRCH", | |
| "ETIME", | |
| "ETIMEDOUT", | |
| "ETXTBSY", | |
| "EWOULDBLOCK", | |
| "EXDEV", | |
| "EXIT_FAILURE", | |
| "EXIT_SUCCESS", | |
| "FE_ALL_EXCEPT", | |
| "FE_DFL_ENV", | |
| "FE_DIVBYZERO", | |
| "FE_DOWNWARD", | |
| "FE_INEXACT", | |
| "FE_INVALID", | |
| "FE_OVERFLOW", | |
| "FE_TONEAREST", | |
| "FE_TOWARDZERO", | |
| "FE_UNDERFLOW", | |
| "FE_UPWARD", | |
| "FILENAME_MAX", | |
| "FLT_DECIMAL_DIG", | |
| "FLT_DIG", | |
| "FLT_EPSILON", | |
| "FLT_EVAL_METHOD", | |
| "FLT_HAS_SUBNORM", | |
| "FLT_MANT_DIG", | |
| "FLT_MAX", | |
| "FLT_MAX_10_EXP", | |
| "FLT_MAX_EXP", | |
| "FLT_MIN", | |
| "FLT_MIN_10_EXP", | |
| "FLT_MIN_EXP", | |
| "FLT_RADIX", | |
| "FLT_ROUNDS", | |
| "FLT_TRUE_MIN", | |
| "FOPEN_MAX", | |
| "FP_FAST_FMA", | |
| "FP_FAST_FMAF", | |
| "FP_FAST_FMAL", | |
| "FP_ILOGB0", | |
| "FP_ILOGBNAN", | |
| "FP_SUBNORMAL", | |
| "FP_ZERO", | |
| "FP_INFINITE", | |
| "FP_NAN", | |
| "FP_NORMAL", | |
| "HUGE_VAL", | |
| "HUGE_VALF", | |
| "HUGE_VALL", | |
| "INFINITY", | |
| "INT_FAST16_MAX", | |
| "INT_FAST16_MIN", | |
| "INT_FAST32_MAX", | |
| "INT_FAST32_MIN", | |
| "INT_FAST64_MAX", | |
| "INT_FAST64_MIN", | |
| "INT_FAST8_MAX", | |
| "INT_FAST8_MIN", | |
| "INT_LEAST16_MAX", | |
| "INT_LEAST16_MIN", | |
| "INT_LEAST32_MAX", | |
| "INT_LEAST32_MIN", | |
| "INT_LEAST64_MAX", | |
| "INT_LEAST64_MIN", | |
| "INT_LEAST8_MAX", | |
| "INT_LEAST8_MIN", | |
| "INT_MAX", | |
| "INT_MIN", | |
| "INT_WIDTH", | |
| "INT16_C", | |
| "INT16_MAX", | |
| "INT16_MIN", | |
| "INT32_C", | |
| "INT32_MAX", | |
| "INT32_MIN", | |
| "INT64_C", | |
| "INT64_MAX", | |
| "INT64_MIN", | |
| "INT8_C", | |
| "INT8_MAX", | |
| "INT8_MIN", | |
| "INTMAX_C", | |
| "INTMAX_MAX", | |
| "INTMAX_MIN", | |
| "INTPTR_MAX", | |
| "INTPTR_MIN", | |
| "L_tmpnam", | |
| "LC_ALL", | |
| "LC_COLLATE", | |
| "LC_CTYPE", | |
| "LC_MONETARY", | |
| "LC_NUMERIC", | |
| "LC_TIME", | |
| "LDBL_DECIMAL_DIG", | |
| "LDBL_DIG", | |
| "LDBL_EPSILON", | |
| "LDBL_HAS_SUBNORM", | |
| "LDBL_MANT_DIG", | |
| "LDBL_MAX", | |
| "LDBL_MAX_10_EXP", | |
| "LDBL_MAX_EXP", | |
| "LDBL_MIN", | |
| "LDBL_MIN_10_EXP", | |
| "LDBL_MIN_EXP", | |
| "LDBL_TRUE_MIN", | |
| "LLONG_MAX", | |
| "LLONG_MIN", | |
| "LLONG_WIDTH", | |
| "LONG_MAX", | |
| "LONG_MIN", | |
| "LONG_WIDTH", | |
| "MATH_ERREXCEPT", | |
| "math_errhandling", | |
| "MATH_ERRNO", | |
| "MB_CUR_MAX", | |
| "MB_LEN_MAX", | |
| "NAN", | |
| "NULL", | |
| "offsetof", | |
| "ONCE_FLAG_INIT", | |
| "PRId16", | |
| "PRId32", | |
| "PRId64", | |
| "PRId8", | |
| "PRIdFAST16", | |
| "PRIdFAST32", | |
| "PRIdFAST64", | |
| "PRIdFAST8", | |
| "PRIdLEAST16", | |
| "PRIdLEAST32", | |
| "PRIdLEAST64", | |
| "PRIdLEAST8", | |
| "PRIdMAX", | |
| "PRIdPTR", | |
| "PRIi16", | |
| "PRIi32", | |
| "PRIi64", | |
| "PRIi8", | |
| "PRIiFAST16", | |
| "PRIiFAST32", | |
| "PRIiFAST64", | |
| "PRIiFAST8", | |
| "PRIiLEAST16", | |
| "PRIiLEAST32", | |
| "PRIiLEAST64", | |
| "PRIiLEAST8", | |
| "PRIiMAX", | |
| "PRIiPTR", | |
| "PRIo16", | |
| "PRIo32", | |
| "PRIo64", | |
| "PRIo8", | |
| "PRIoFAST16", | |
| "PRIoFAST32", | |
| "PRIoFAST64", | |
| "PRIoFAST8", | |
| "PRIoLEAST16", | |
| "PRIoLEAST32", | |
| "PRIoLEAST64", | |
| "PRIoLEAST8", | |
| "PRIoMAX", | |
| "PRIoPTR", | |
| "PRIu16", | |
| "PRIu32", | |
| "PRIu64", | |
| "PRIu8", | |
| "PRIuFAST16", | |
| "PRIuFAST32", | |
| "PRIuFAST64", | |
| "PRIuFAST8", | |
| "PRIuLEAST16", | |
| "PRIuLEAST32", | |
| "PRIuLEAST64", | |
| "PRIuLEAST8", | |
| "PRIuMAX", | |
| "PRIuPTR", | |
| "PRIx16", | |
| "PRIX16", | |
| "PRIx32", | |
| "PRIX32", | |
| "PRIx64", | |
| "PRIX64", | |
| "PRIx8", | |
| "PRIX8", | |
| "PRIxFAST16", | |
| "PRIXFAST16", | |
| "PRIxFAST32", | |
| "PRIXFAST32", | |
| "PRIxFAST64", | |
| "PRIXFAST64", | |
| "PRIxFAST8", | |
| "PRIXFAST8", | |
| "PRIxLEAST16", | |
| "PRIXLEAST16", | |
| "PRIxLEAST32", | |
| "PRIXLEAST32", | |
| "PRIxLEAST64", | |
| "PRIXLEAST64", | |
| "PRIxLEAST8", | |
| "PRIXLEAST8", | |
| "PRIxMAX", | |
| "PRIXMAX", | |
| "PRIxPTR", | |
| "PRIXPTR", | |
| "PTRDIFF_MAX", | |
| "PTRDIFF_MIN", | |
| "RAND_MAX", | |
| "SCHAR_MAX", | |
| "SCHAR_MIN", | |
| "SCHAR_WIDTH", | |
| "SCNd16", | |
| "SCNd32", | |
| "SCNd64", | |
| "SCNd8", | |
| "SCNdFAST16", | |
| "SCNdFAST32", | |
| "SCNdFAST64", | |
| "SCNdFAST8", | |
| "SCNdLEAST16", | |
| "SCNdLEAST32", | |
| "SCNdLEAST64", | |
| "SCNdLEAST8", | |
| "SCNdMAX", | |
| "SCNdPTR", | |
| "SCNi16", | |
| "SCNi32", | |
| "SCNi64", | |
| "SCNi8", | |
| "SCNiFAST16", | |
| "SCNiFAST32", | |
| "SCNiFAST64", | |
| "SCNiFAST8", | |
| "SCNiLEAST16", | |
| "SCNiLEAST32", | |
| "SCNiLEAST64", | |
| "SCNiLEAST8", | |
| "SCNiMAX", | |
| "SCNiPTR", | |
| "SCNo16", | |
| "SCNo32", | |
| "SCNo64", | |
| "SCNo8", | |
| "SCNoFAST16", | |
| "SCNoFAST32", | |
| "SCNoFAST64", | |
| "SCNoFAST8", | |
| "SCNoLEAST16", | |
| "SCNoLEAST32", | |
| "SCNoLEAST64", | |
| "SCNoLEAST8", | |
| "SCNoMAX", | |
| "SCNoPTR", | |
| "SCNu16", | |
| "SCNu32", | |
| "SCNu64", | |
| "SCNu8", | |
| "SCNuFAST16", | |
| "SCNuFAST32", | |
| "SCNuFAST64", | |
| "SCNuFAST8", | |
| "SCNuLEAST16", | |
| "SCNuLEAST32", | |
| "SCNuLEAST64", | |
| "SCNuLEAST8", | |
| "SCNuMAX", | |
| "SCNuPTR", | |
| "SCNx16", | |
| "SCNx32", | |
| "SCNx64", | |
| "SCNx8", | |
| "SCNxFAST16", | |
| "SCNxFAST32", | |
| "SCNxFAST64", | |
| "SCNxFAST8", | |
| "SCNxLEAST16", | |
| "SCNxLEAST32", | |
| "SCNxLEAST64", | |
| "SCNxLEAST8", | |
| "SCNxMAX", | |
| "SCNxPTR", | |
| "SEEK_CUR", | |
| "SEEK_END", | |
| "SEEK_SET", | |
| "setjmp", | |
| "SHRT_MAX", | |
| "SHRT_MIN", | |
| "SHRT_WIDTH", | |
| "SIG_ATOMIC_MAX", | |
| "SIG_ATOMIC_MIN", | |
| "SIG_DFL", | |
| "SIG_ERR", | |
| "SIG_IGN", | |
| "SIGABRT", | |
| "SIGFPE", | |
| "SIGILL", | |
| "SIGINT", | |
| "SIGSEGV", | |
| "SIGTERM", | |
| "SIZE_MAX", | |
| "stderr", | |
| "stdin", | |
| "stdout", | |
| "TIME_UTC", | |
| "TMP_MAX", | |
| "UCHAR_MAX", | |
| "UCHAR_WIDTH", | |
| "UINT_FAST16_MAX", | |
| "UINT_FAST32_MAX", | |
| "UINT_FAST64_MAX", | |
| "UINT_FAST8_MAX", | |
| "UINT_LEAST16_MAX", | |
| "UINT_LEAST32_MAX", | |
| "UINT_LEAST64_MAX", | |
| "UINT_LEAST8_MAX", | |
| "UINT_MAX", | |
| "UINT_WIDTH", | |
| "UINT16_C", | |
| "UINT16_MAX", | |
| "UINT32_C", | |
| "UINT32_MAX", | |
| "UINT64_MAX", | |
| "UINT64_C", | |
| "UINT8_C", | |
| "UINT8_MAX", | |
| "UINTMAX_C", | |
| "UINTMAX_MAX", | |
| "UINTPTR_MAX", | |
| "ULLONG_MAX", | |
| "ULLONG_WIDTH", | |
| "ULONG_MAX", | |
| "ULONG_WIDTH", | |
| "USHRT_MAX", | |
| "USHRT_WIDTH", | |
| "va_arg", | |
| "va_copy", | |
| "va_end", | |
| "va_start", | |
| "WCHAR_MAX", | |
| "WCHAR_MIN", | |
| "WEOF", | |
| "WINT_MAX", | |
| "WINT_MIN", | |
| }; | |
| RESERVED_CC_MACROS.contains(ident) | |
| } | |
| /// If `ident` is reserved in C++, returns a string with an underscore appended to it. | |
| /// Otherwise, returns `ident`. | |
| pub fn unkeyword_cpp_ident(ident: &str, features: FlagSet<CrubitFeature>) -> Cow<'_, str> { | |
| if is_cpp_nonportable_word(ident, features) { | |
| Cow::Owned(format!("{ident}_")) | |
| } else { | |
| Cow::Borrowed(ident) | |
| } | |
| } | |
| /// Formats a C++ identifier. Panics if `ident` is a C++ reserved word. | |
| /// | |
| /// This should only be used for generated identifiers that we control, and can guarantee do not | |
| /// collide with reserved words in the standard. | |
| #[track_caller] | |
| pub fn expect_format_cc_ident(ident: &str) -> Ident { | |
| format_cc_ident(ident, FlagSet::<CrubitFeature>::full()) | |
| .unwrap_or_else(|err| panic!("Can't format `{ident}` as a C++ identifier: {err}")) | |
| } | |
| /// Fallibly parses a [`proc_macro2::Ident`] where Rust keywords are permitted. | |
| /// | |
| /// * Unlike [`Ident::new`], this is fallible and returns Err if it fails. | |
| /// * Unlike [`syn::parse_str::<Ident>`], Rust keywords are permitted, making this suitable for | |
| /// parsing identifiers that that Rust doesn't allow but C++ does, like "move". | |
| fn parse_any(ident: &str) -> syn::Result<Ident> { | |
| syn::parse::Parser::parse_str(<Ident as syn::ext::IdentExt>::parse_any, ident) | |
| } | |
| /// Formats a C++ (qualified) identifier. Returns an error when `ident` is a C++ | |
| /// reserved keyword or is an invalid identifier. | |
| pub fn format_cc_ident(ident: &str, features: FlagSet<CrubitFeature>) -> Result<Ident> { | |
| ensure!( | |
| !is_cpp_nonportable_word(ident, features), | |
| "`{ident}` is a C++ reserved word and can't be used as a C++ identifier", | |
| ); | |
| unchecked_format_cc_ident(ident) | |
| } | |
| /// Formats a C++ (qualified) identifier. Returns an error when `ident` is a C++ | |
| /// an invalid identifier. | |
| /// | |
| /// Use this function when the identifier originally came from C++, otherwise, use | |
| /// `format_cc_ident`. | |
| pub fn format_nonportable_cc_ident(ident: &str) -> Result<Ident> { | |
| ensure!( | |
| !is_cpp_reserved_keyword(ident), | |
| "`{ident}` is a C++ reserved word and can't be used as a C++ identifier", | |
| ); | |
| unchecked_format_cc_ident(ident) | |
| } | |
| fn unchecked_format_cc_ident(ident: &str) -> Result<Ident> { | |
| ensure!(!ident.is_empty(), "Empty string is not a valid C++ identifier"); | |
| // Explicitly mapping the error via `anyhow!`, because `LexError` is not `Sync` | |
| // (required for `anyhow::Error` to implement `From<LexError>`) and | |
| // therefore we can't just use `?`. | |
| parse_any(ident) | |
| .map_err(|lex_error| anyhow!("Can't format `{ident}` as a C++ identifier: {lex_error}")) | |
| } | |
| /// Formats a C++ type name. Returns an error when `name` is a C++ | |
| /// reserved keyword or otherwise an invalid type name. | |
| pub fn format_cc_type_name(name: &str, features: FlagSet<CrubitFeature>) -> Result<TokenStream> { | |
| check_portable_cc_name(name, features)?; | |
| unchecked_format_cc_type_name(name) | |
| } | |
| pub fn format_nonportable_cc_type_name(name: &str) -> Result<TokenStream> { | |
| check_valid_cc_name(name)?; | |
| unchecked_format_cc_type_name(name) | |
| } | |
| fn unchecked_format_cc_type_name(name: &str) -> Result<TokenStream> { | |
| match name.parse() { | |
| Ok(name) => Ok(name), | |
| Err(_) => { | |
| // Sometimes valid C++ type names do not parse as TokenStreams. | |
| // For example, this can happen in the case of literals that are not | |
| // valid Rust: IntTemplateStruct<'\f'>. | |
| // | |
| // As a last ditch effort, we convert the identifier into a string literal. | |
| // We could use quote! to do the escaping, but instead we do it manually to | |
| // mirror the code in token_stream_printer. | |
| let name = name.replace("\\", "\\\\").replace('"', "\\\""); | |
| let name = format!("__LITERALLY__ \"{name}\""); | |
| name.parse() | |
| .map_err(|lex_error| anyhow!("Can't format `{name}` as a C++ type: {lex_error}")) | |
| } | |
| } | |
| } | |
| /// Converts a hyphen-separated string to an underscore-separated string. | |
| /// | |
| /// Panics if `s` is empty. | |
| fn hyphen_to_underscore(s: &str) -> Cow<'_, str> { | |
| let mut parts = s.split('-'); | |
| let first_part = parts.next().expect("at least one part expected"); | |
| let Some(second_part) = parts.next() else { | |
| return Cow::Borrowed(first_part); | |
| }; | |
| let mut owned = String::with_capacity(s.len()); | |
| let _ = write!(&mut owned, "{first_part}_{second_part}"); | |
| for part in parts { | |
| let _ = write!(&mut owned, "_{part}"); | |
| } | |
| Cow::Owned(owned) | |
| } | |
| /// Makes an 'Ident' to be used in the Rust source code. Escapes Rust keywords. | |
| /// Panics if `ident` is empty or is otherwise an invalid identifier. | |
| /// | |
| /// Hyphens are converted to underscores in the identifier. | |
| pub fn make_rs_ident(ident: &str) -> Ident { | |
| try_make_rs_ident(ident).expect("Failed to make Rust identifier") | |
| } | |
| /// Makes an 'Ident' to be used in the Rust source code. Escapes Rust keywords. | |
| /// Returns an error if `ident` is empty or is otherwise an invalid identifier. | |
| /// | |
| /// Hyphens are converted to underscores in the identifier. | |
| pub fn try_make_rs_ident(ident: &str) -> Result<Ident> { | |
| // Target names, which become crate names, may sometimes contain hyphens. | |
| // Since identifiers cannot contain hyphens, we convert them to underscores. | |
| let ident = hyphen_to_underscore(ident); | |
| // TODO(https://github.com/dtolnay/syn/pull/1098): Remove the hardcoded list once syn recognizes | |
| // newly added keywords. | |
| // NOTE: the above PR was accepted for 2021 and 2018 editions, but `try` still isn't escaped, | |
| // so we may need to tweak something. | |
| if matches!(&*ident, "gen" | "async" | "await" | "try" | "dyn") { | |
| return Ok(format_ident!("r#{}", ident)); | |
| } | |
| syn::parse_str(&ident).or_else(|_| syn::parse_str(&format!("r#{ident}"))).map_err(Into::into) | |
| } | |
| /// Makes a 'Lifetime' to be used in the Rust source code as a lifetime name. | |
| /// Panics if `ident` is empty or is otherwise an invalid identifier. | |
| /// | |
| /// Hyphens are converted to underscores in the identifier. | |
| pub fn make_rs_lifetime_ident(ident: &str) -> syn::Lifetime { | |
| if ident == "_" || ident == "static" { | |
| return syn::Lifetime::new(&format!("'{ident}"), proc_macro2::Span::call_site()); | |
| } | |
| syn::Lifetime { apostrophe: proc_macro2::Span::call_site(), ident: make_rs_ident(ident) } | |
| } | |
| fn check_valid_cc_name_impl(name: &str) -> Result<()> { | |
| // https://en.cppreference.com/w/cpp/language/identifiers says that "A valid identifier must | |
| // begin with a non-digit character (Latin letter, underscore, or Unicode | |
| // character of class XID_Start)". One motivation for this check is to | |
| // explicitly catch names of tuple fields (e.g. `some_tuple.0`). | |
| let first_char = | |
| name.chars().next().ok_or_else(|| anyhow!("Empty string is not a valid C++ identifier"))?; | |
| ensure!( | |
| unicode_ident::is_xid_start(first_char) || first_char == '_' || first_char == ':', | |
| "The following character can't be used as a start of a C++ identifier: {first_char}", | |
| ); | |
| Ok(()) | |
| } | |
| /// Checks that `name` is a valid C++ identifier. | |
| pub fn check_valid_cc_name(name: &str) -> Result<()> { | |
| ensure!( | |
| !is_cpp_reserved_keyword(name), | |
| "`{name}` is a C++ reserved keyword and can't be used as a C++ identifier", | |
| ); | |
| check_valid_cc_name_impl(name) | |
| } | |
| /// Checks that `name` is a valid C++ identifier on typical platforms. | |
| /// | |
| /// This also rejects names that collide with macros defined in the C++ standard library. | |
| pub fn check_portable_cc_name(name: &str, features: FlagSet<CrubitFeature>) -> Result<()> { | |
| ensure!( | |
| !is_cpp_nonportable_word(name, features), | |
| "`{name}` is a C++ reserved word and can't be used as a C++ identifier", | |
| ); | |
| check_valid_cc_name_impl(name) | |
| } | |
| /// Escapes characters that may not appear in a C++ or Rust identifier. | |
| /// | |
| /// The implemented escaping algorithm guarantess that different inputs will | |
| /// always produce different outputs (i.e. unique symbols will remain unique | |
| /// after escaping). Other than that, the implemented escaping algorithm is | |
| /// somewhat arbitrary and should be treated as an implementation detail and not | |
| /// depended upon. | |
| /// | |
| /// This transformation allows using escaped symbol names as part of Rust and/or | |
| /// C++ identifiers. In particular note that in practice Rust uses `$` and `.` | |
| /// characters in symbols - for example: "_ZN58_$LT$rust_out.. | |
| /// Point$u20$as$u20$core..default..Default$GT$7default17h144069f0ad7be325E". | |
| pub fn escape_non_identifier_chars(symbol: &str) -> String { | |
| // EXTRA_CAPACITY_PREDICTION has been haphazardly chosen based on a single | |
| // example encountered in practice where there were 16 characters that needed | |
| // escaping: 2 x '_', 8 x '$', 6 x '.': "_ZN58_$LT$rust_out.. | |
| // Point$u20$as$u20$core..default..Default$GT$7default17h144069f0ad7be325E" | |
| const EXTRA_CAPACITY_PREDICTION: usize = 20; | |
| let mut result = String::with_capacity(symbol.len() + EXTRA_CAPACITY_PREDICTION); | |
| for (i, c) in symbol.chars().enumerate() { | |
| match c { | |
| '_' => result.push_str("_u"), | |
| '$' => result.push_str("_d"), | |
| '.' => result.push_str("_p"), | |
| c => { | |
| let is_valid_identifier_char = if i == 0 { | |
| // `is_xid_start` doesn't cover `'_'` character, but it is okay because we | |
| // explicitly handle this character in a match branch above. | |
| unicode_ident::is_xid_start(c) | |
| } else { | |
| unicode_ident::is_xid_continue(c) | |
| }; | |
| if is_valid_identifier_char { | |
| result.push(c); | |
| } else { | |
| _ = write!(&mut result, "_x{:08x}", c as u32); | |
| }; | |
| } | |
| } | |
| } | |
| result | |
| } | |
| /// Representation of `foo::bar::baz` where each component is either the name | |
| /// of a C++ namespace, or the name of a Rust module. | |
| #[derive(Debug, PartialEq, Eq, Clone, Hash, PartialOrd, Ord)] | |
| pub struct NamespaceQualifier { | |
| // Outer to innermost | |
| pub namespaces: Vec<Rc<str>>, | |
| // Outer to innermost. Paired as (rs_name, cc_name) | |
| pub nested_records: Vec<(Rc<str>, Rc<str>)>, | |
| /// Whether to prepend `::` when formatting for C++. | |
| // TODO(b/502939407): Remove this field (it will always be true). | |
| pub use_leading_colons: bool, | |
| } | |
| impl NamespaceQualifier { | |
| /// Constructs a new `NamespaceQualifier` from a sequence of names. | |
| pub fn new<T: Into<Rc<str>>>( | |
| iter: impl IntoIterator<Item = T>, | |
| use_leading_colons: bool, | |
| ) -> Self { | |
| // TODO(b/258265044): Catch most (all if possible) error conditions early. For | |
| // example: | |
| // - Panic early if any strings are empty, or are not Rust identifiers | |
| // - Report an error early if any strings are C++ reserved keywords | |
| // This may make `format_for_cc` and `format_namespace_bound_cc_tokens` infallible. | |
| Self { | |
| namespaces: iter.into_iter().map(Into::into).collect(), | |
| nested_records: vec![], | |
| use_leading_colons, | |
| } | |
| } | |
| pub fn is_empty(&self) -> bool { | |
| self.namespaces.is_empty() && self.nested_records.is_empty() | |
| } | |
| /// Returns an iterator over the Rust modules in the namespace qualifier. | |
| /// Parent records are converted to snake case. | |
| pub fn parts_with_snake_case_record_names(&self) -> impl Iterator<Item = Ident> + use<'_> { | |
| self.namespaces.iter().map(|ns| make_rs_ident(ns)).chain( | |
| self.nested_records | |
| .iter() | |
| .map(|(rs_name, _cc_name)| make_rs_ident(&rs_name.to_string().to_snake_case())), | |
| ) | |
| } | |
| pub fn parts(&self) -> impl Iterator<Item = &Rc<str>> + use<'_> { | |
| self.namespaces.iter().chain(self.nested_records.iter().map(|(_rs_name, cc_name)| cc_name)) | |
| } | |
| /// Returns `foo::bar::baz::` (escaping Rust keywords as needed). | |
| pub fn format_for_rs(&self) -> TokenStream { | |
| let rust_parts = self.parts_with_snake_case_record_names(); | |
| quote! { #(#rust_parts::)* } | |
| } | |
| /// Returns `foo::bar::baz::` (reporting errors for C++ keywords). | |
| pub fn format_for_cc(&self, features: FlagSet<CrubitFeature>) -> Result<TokenStream> { | |
| let mut path = if self.use_leading_colons { | |
| quote! { :: } | |
| } else { | |
| quote! {} | |
| }; | |
| for namespace in &self.namespaces { | |
| let namespace = format_cc_ident(namespace, features)?; | |
| path.extend(quote! { #namespace :: }); | |
| } | |
| for (_rs_name, cc_name) in &self.nested_records { | |
| let cc_name = format_cc_type_name(cc_name, features)?; | |
| path.extend(quote! { #cc_name ::}); | |
| } | |
| Ok(path) | |
| } | |
| /// Returns `foo::bar::baz::` (never reporting errors). | |
| pub fn format_for_cc_debug(&self) -> String { | |
| let mut path = String::new(); | |
| for part in self.parts() { | |
| path.push_str(part); | |
| path.push_str("::"); | |
| } | |
| path | |
| } | |
| } | |
| /// `CcInclude` represents a single `#include ...` directive in C++. | |
| #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] | |
| pub enum CcInclude { | |
| /// Represents a system header, e.g., `cstdef`, which will be included by | |
| /// angular brackets. | |
| SystemHeader(Rc<str>), | |
| /// Represents a user header, which will be included by quotes. | |
| UserHeader(Rc<str>), | |
| /// Represents a user header which should be re-exported by IWYU. | |
| ExportedUserHeader(Rc<str>), | |
| /// Represents an `#include` for Crubit C++ support library headers: the | |
| /// format specifier for what comes after `#include` and path of the support | |
| /// library header. | |
| SupportLibHeader(Format<1>, Rc<str>), | |
| } | |
| impl CcInclude { | |
| /// Creates a `CcInclude` that represents `#include <array>` and provides | |
| /// C++ types like `std::array`. | |
| /// https://en.cppreference.com/w/cpp/header/array | |
| pub fn array() -> Self { | |
| Self::SystemHeader("array".into()) | |
| } | |
| /// Creates a `CcInclude` that represents `#include <compare>` and provides | |
| /// C++ types like `std::strong_ordering`. | |
| /// https://en.cppreference.com/w/cpp/header/compare | |
| pub fn compare() -> Self { | |
| Self::SystemHeader("compare".into()) | |
| } | |
| /// Creates a `CcInclude` that represents `#include <cstddef>` and provides | |
| /// C++ types like `std::size_t` or `std::ptrdiff_t`. See | |
| /// https://en.cppreference.com/w/cpp/header/cstddef | |
| pub fn cstddef() -> Self { | |
| Self::SystemHeader("cstddef".into()) | |
| } | |
| /// Creates a `CcInclude` that represents `#include <cstdint>` and provides | |
| /// C++ types like `std::int16_t` or `std::uint32_t`. See | |
| /// https://en.cppreference.com/w/cpp/header/cstdint | |
| pub fn cstdint() -> Self { | |
| Self::SystemHeader("cstdint".into()) | |
| } | |
| /// Creates a `CcInclude` that represents `#include <cstring>` and provides | |
| /// C++ methods like `std::memcpy`. | |
| /// https://en.cppreference.com/w/cpp/header/cstring | |
| pub fn cstring() -> Self { | |
| Self::SystemHeader("cstring".into()) | |
| } | |
| /// Creates a `CcInclude` that represents `#include <memory>`. | |
| /// See https://en.cppreference.com/w/cpp/header/memory | |
| pub fn memory() -> Self { | |
| Self::SystemHeader("memory".into()) | |
| } | |
| /// Creates a `CcInclude` that represents `#include <utility>` and provides | |
| /// C++ functions like `std::move` and C++ types like `std::tuple`. | |
| /// See https://en.cppreference.com/w/cpp/header/utility | |
| pub fn utility() -> Self { | |
| Self::SystemHeader("utility".into()) | |
| } | |
| /// Creates a `CcInclude` that represents `#include <tuple>` and provides | |
| /// C++ `std::tuple`, `std::tie`, etc. | |
| /// See https://en.cppreference.com/w/cpp/header/utility | |
| pub fn tuple() -> Self { | |
| Self::SystemHeader("tuple".into()) | |
| } | |
| /// Creates a `CcInclude` that represents `#include <optional>` and provides | |
| /// C++ `std::optional`. | |
| /// See https://en.cppreference.com/w/cpp/header/optional | |
| pub fn optional() -> Self { | |
| Self::SystemHeader("optional".into()) | |
| } | |
| /// Creates a `CcInclude` that represents `#include <bit>` and provides | |
| /// C++ functions like `std::bit_cast`. | |
| /// See https://en.cppreference.com/w/cpp/header/bit | |
| pub fn bit() -> Self { | |
| Self::SystemHeader("bit".into()) | |
| } | |
| /// Creates a `CcInclude` that represents `#include <new>` and provides | |
| /// placement new. | |
| /// See https://en.cppreference.com/w/cpp/header/new | |
| pub fn new_header() -> Self { | |
| Self::SystemHeader("new".into()) | |
| } | |
| /// Creates a `CcInclude` that represents `#include <type_traits>` and | |
| /// provides C++ APIs like `std::is_trivially_copy_constructible_v`. | |
| /// See https://en.cppreference.com/w/cpp/header/type_traits | |
| pub fn type_traits() -> Self { | |
| Self::SystemHeader("type_traits".into()) | |
| } | |
| /// Creates a user include: `#include "some/path/to/header.h"`. | |
| pub fn user_header(path: Rc<str>) -> Self { | |
| Self::UserHeader(path) | |
| } | |
| /// Creates a user include: `#include "some/path/to/header.h" // IWYU pragma: export` | |
| pub fn exported_user_header(path: Rc<str>) -> Self { | |
| Self::ExportedUserHeader(path) | |
| } | |
| /// Creates a `CcInclude` and detects whether it's a system header or a user | |
| /// header based on the path. | |
| /// | |
| /// System headers are included by angular brackets, e.g., `#include <cstddef>`. | |
| /// User headers are included by quotes, e.g., `#include "some/path/to/header.h"`. | |
| pub fn from_path(path: &str) -> Self { | |
| match (path.starts_with("<"), path.ends_with(">")) { | |
| (true, true) => Self::SystemHeader(Rc::from(&path[1..path.len() - 1])), | |
| _ => Self::UserHeader(Rc::from(path)), | |
| } | |
| } | |
| /// Creates a support library header include based on the specified format. | |
| /// E.g., `\"{header}\"` and `hdr.h` produces `#include "hdr.h"`. | |
| pub fn support_lib_header(format: Format<1>, path: Rc<str>) -> Self { | |
| Self::SupportLibHeader(format, path) | |
| } | |
| } | |
| impl ToTokens for CcInclude { | |
| fn to_tokens(&self, tokens: &mut TokenStream) { | |
| match self { | |
| Self::SystemHeader(path) => { | |
| let path: TokenStream = path | |
| .parse() | |
| .expect("`pub` API of `CcInclude` guarantees validity of system includes"); | |
| quote! { __HASH_TOKEN__ include < #path > __NEWLINE__ }.to_tokens(tokens) | |
| } | |
| Self::UserHeader(path) => { | |
| quote! { __HASH_TOKEN__ include #path __NEWLINE__ }.to_tokens(tokens) | |
| } | |
| Self::ExportedUserHeader(path) => { | |
| quote! { __HASH_TOKEN__ include #path __COMMENT__ "IWYU pragma: export" __NEWLINE__ }.to_tokens(tokens) | |
| } | |
| Self::SupportLibHeader(format, path) => { | |
| let full_path: TokenStream = format | |
| .format(&[path.as_ref()]) | |
| .parse() | |
| .expect("Failed to parse support lib `#include` path"); | |
| quote! { __HASH_TOKEN__ include #full_path __NEWLINE__ }.to_tokens(tokens) | |
| } | |
| } | |
| } | |
| } | |
| /// Formats a set of `CcInclude`s, trying to follow the guidance from | |
| /// [the Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html#Names_and_Order_of_Includes). | |
| pub fn format_cc_includes(set_of_includes: &BTreeSet<CcInclude>) -> TokenStream { | |
| let mut tokens = TokenStream::default(); | |
| let mut iter = set_of_includes | |
| .iter() | |
| .filter(|include| match include { | |
| CcInclude::UserHeader(path) => { | |
| !set_of_includes.contains(&CcInclude::ExportedUserHeader(path.clone())) | |
| } | |
| _ => true, | |
| }) | |
| .peekable(); | |
| while let Some(include) = iter.next() { | |
| include.to_tokens(&mut tokens); | |
| // Add an empty line between system headers and user headers. | |
| if let ( | |
| CcInclude::SystemHeader(_), | |
| Some(CcInclude::UserHeader(_) | CcInclude::ExportedUserHeader(_)), | |
| ) = (include, iter.peek()) | |
| { | |
| quote! { __NEWLINE__ }.to_tokens(&mut tokens) | |
| } | |
| } | |
| tokens | |
| } | |
| #[cfg(test)] | |
| pub mod tests { | |
| use super::*; | |
| use googletest::matchers::starts_with; | |
| use googletest::{expect_that, gtest}; | |
| use itertools::Itertools; | |
| use quote::{quote, ToTokens}; | |
| use token_stream_matchers::{assert_cc_matches, assert_rs_matches}; | |
| use token_stream_printer::cc_tokens_to_formatted_string_for_tests; | |
| #[gtest] | |
| fn test_format_cc_ident_basic() { | |
| assert_cc_matches!( | |
| format_cc_ident("foo", FlagSet::default()).unwrap().to_token_stream(), | |
| quote! { foo } | |
| ); | |
| } | |
| #[gtest] | |
| fn test_format_cc_ident_exotic_xid_start() { | |
| assert_cc_matches!( | |
| format_cc_ident("Łukasz", FlagSet::default()).unwrap().to_token_stream(), | |
| quote! { Łukasz } | |
| ); | |
| } | |
| #[gtest] | |
| fn test_format_cc_ident_underscore() { | |
| assert_cc_matches!( | |
| format_cc_ident("_", FlagSet::default()).unwrap().to_token_stream(), | |
| quote! { _ } | |
| ); | |
| } | |
| #[gtest] | |
| fn test_format_cc_ident_reserved_rust_keyword() { | |
| assert_cc_matches!( | |
| format_cc_ident("impl", FlagSet::default()).unwrap().to_token_stream(), | |
| quote! { impl } | |
| ); | |
| } | |
| #[gtest] | |
| fn test_format_cc_ident_reserved_cc_keyword() { | |
| let err = format_cc_ident("reinterpret_cast", FlagSet::default()).unwrap_err(); | |
| let msg = err.to_string(); | |
| assert!(msg.contains("`reinterpret_cast`")); | |
| assert!(msg.contains("C++ reserved word")); | |
| } | |
| #[gtest] | |
| fn test_format_cc_ident_reserved_standard_macros() { | |
| assert_cc_matches!( | |
| format_cc_ident("stdin", FlagSet::default()).unwrap().to_token_stream(), | |
| quote! { stdin } | |
| ); | |
| let err = | |
| format_cc_ident("stdin", CrubitFeature::ReserveStandardMacros.into()).unwrap_err(); | |
| let msg = err.to_string(); | |
| assert!(msg.contains("`stdin`")); | |
| assert!(msg.contains("C++ reserved word")); | |
| } | |
| #[gtest] | |
| fn test_format_cc_ident_unqualified_identifiers() { | |
| // https://en.cppreference.com/w/cpp/language/identifiers#Unqualified_identifiers | |
| // These may appear in `IR::Func::name`. | |
| assert_cc_matches!( | |
| format_cc_type_name("operator==", FlagSet::default()).unwrap(), | |
| quote! { operator== } | |
| ); | |
| assert_cc_matches!( | |
| format_cc_type_name("operator new", FlagSet::default()).unwrap(), | |
| quote! { operator new } | |
| ); | |
| // This may appear in `IR::Record::cc_name` (although in practice these will | |
| // be namespace-qualified most of the time). | |
| assert_cc_matches!( | |
| format_cc_type_name("MyTemplate<int>", FlagSet::default()).unwrap(), | |
| quote! { MyTemplate<int> } | |
| ); | |
| } | |
| /// https://en.cppreference.com/w/cpp/language/identifiers#Qualified_identifiers | |
| /// | |
| /// This may appear in `IR::Record::cc_name`, or in | |
| /// `crubit_annotate::cpp_layout_equivalent(cpp_type=...)`. | |
| #[gtest] | |
| fn test_format_cc_ident_qualified_identifiers() { | |
| assert_cc_matches!( | |
| format_cc_type_name("std::vector<int>", FlagSet::default()).unwrap(), | |
| quote! { std::vector<int> } | |
| ); | |
| assert_cc_matches!( | |
| format_cc_type_name("::std::vector<int>", FlagSet::default()).unwrap(), | |
| quote! { ::std::vector<int> } | |
| ); | |
| } | |
| #[gtest] | |
| fn test_format_cc_ident_empty() { | |
| let err = format_cc_ident("", FlagSet::default()).unwrap_err(); | |
| let msg = err.to_string(); | |
| assert_eq!(msg, "Empty string is not a valid C++ identifier"); | |
| } | |
| #[gtest] | |
| fn test_format_cc_ident_invalid_first_char() { | |
| let tests = vec![ | |
| // `0` and `1 are field names in `struct RustStruct(i32, u16)`. | |
| "0", | |
| // `~MyClass` is a valid unqualified identifier in C++, but it is okay if | |
| // `format_cc_ident` rejects it, because `format_cc_ident` is not used to format | |
| // destructor names. | |
| "~MyClass", | |
| // We used to trim leading and/or trailing whitespace, but stricter validation | |
| // of leading whitespace seems desirable. | |
| r#" operator "" _km "#, | |
| // Other tests | |
| "(foo", | |
| "(foo)", | |
| ]; | |
| for test in tests.into_iter() { | |
| let err = format_cc_ident(test, FlagSet::default()).unwrap_err(); | |
| let actual_msg = err.to_string(); | |
| let expected_msg = format!("Can't format `{test}` as a C++ identifier: "); | |
| expect_that!(actual_msg, starts_with(expected_msg)); | |
| } | |
| } | |
| #[gtest] | |
| fn test_make_rs_ident_basic() { | |
| let id = make_rs_ident("foo"); | |
| assert_rs_matches!(quote! { #id }, quote! { foo }); | |
| } | |
| #[gtest] | |
| fn test_make_rs_ident_reserved_cc_keyword() { | |
| let id = make_rs_ident("reinterpret_cast"); | |
| assert_rs_matches!(quote! { #id }, quote! { reinterpret_cast }); | |
| } | |
| #[gtest] | |
| fn test_make_rs_ident_reserved_rust_keyword() { | |
| let id = make_rs_ident("impl"); | |
| assert_rs_matches!(quote! { #id }, quote! { r#impl }); | |
| } | |
| #[gtest] | |
| #[should_panic] | |
| fn test_make_rs_ident_invalid_identifier() { | |
| make_rs_ident("foo!%@^$!^%@$!@bar"); // Invalid Rust identifier. | |
| } | |
| #[gtest] | |
| fn test_try_make_rs_ident_unfinished_group() { | |
| // This test specifically uses `(foo` (instead of some other invalid identifier) | |
| // so that the input string is not just an invalid identifier, but is also an invalid | |
| // token tree. (I vaguely remember that the test was introduced after discovering | |
| // or suspecting that an old implementation was sensitive to this distinction.) | |
| let result = try_make_rs_ident("(foo"); // No closing `)`. | |
| assert!(result.is_err()); | |
| } | |
| #[gtest] | |
| fn test_try_make_rs_ident_empty() { | |
| let result = try_make_rs_ident(""); | |
| assert!(result.is_err()); | |
| } | |
| #[gtest] | |
| fn test_cc_include_to_tokens_for_system_header() { | |
| let include = CcInclude::cstddef(); | |
| assert_cc_matches!( | |
| quote! { #include }, | |
| quote! { | |
| __HASH_TOKEN__ include <cstddef> | |
| } | |
| ); | |
| } | |
| #[gtest] | |
| fn test_cc_include_to_tokens_for_user_header() { | |
| let include = CcInclude::user_header("some/path/to/header.h".into()); | |
| assert_cc_matches!( | |
| quote! { #include }, | |
| quote! { | |
| __HASH_TOKEN__ include "some/path/to/header.h" | |
| } | |
| ); | |
| } | |
| #[gtest] | |
| fn test_cc_include_ord() { | |
| let cstddef = CcInclude::cstddef(); | |
| let memory = CcInclude::memory(); | |
| let a = CcInclude::user_header("a.h".into()); | |
| let b = CcInclude::user_header("b.h".into()); | |
| assert!(cstddef < memory); | |
| assert!(cstddef < a); | |
| assert!(cstddef < b); | |
| assert!(memory < a); | |
| assert!(memory < b); | |
| assert!(a < b); | |
| } | |
| #[gtest] | |
| fn test_format_cc_includes() { | |
| let includes = [ | |
| CcInclude::cstddef(), | |
| CcInclude::memory(), | |
| CcInclude::user_header("a.h".into()), | |
| CcInclude::user_header("b.h".into()), | |
| ] | |
| .into_iter() | |
| .collect::<BTreeSet<_>>(); | |
| let tokens = format_cc_includes(&includes); | |
| let actual = | |
| cc_tokens_to_formatted_string_for_tests(quote! { __NEWLINE__ #tokens }).unwrap(); | |
| assert_eq!( | |
| actual, | |
| r#" | |
| #include <cstddef> | |
| #include <memory> | |
| #include "a.h" | |
| #include "b.h" | |
| "# | |
| ); | |
| } | |
| #[gtest] | |
| fn test_format_cc_includes_exported_user_header_deduplicates_user_header() { | |
| let includes = [ | |
| CcInclude::cstddef(), | |
| CcInclude::user_header("a.h".into()), | |
| CcInclude::exported_user_header("a.h".into()), | |
| CcInclude::user_header("b.h".into()), | |
| ] | |
| .into_iter() | |
| .collect::<BTreeSet<_>>(); | |
| let tokens = format_cc_includes(&includes); | |
| let actual = | |
| cc_tokens_to_formatted_string_for_tests(quote! { __NEWLINE__ #tokens }).unwrap(); | |
| assert_eq!( | |
| actual, | |
| r#" | |
| #include <cstddef> | |
| #include "a.h" // IWYU pragma: export | |
| #include "b.h" | |
| "# | |
| ); | |
| } | |
| #[gtest] | |
| fn test_namespace_qualifier_empty() { | |
| let ns = NamespaceQualifier::new::<&str>([], true); | |
| let actual_rs = ns.format_for_rs(); | |
| assert!(actual_rs.is_empty()); | |
| let actual_cc = ns.format_for_cc(FlagSet::default()).unwrap(); | |
| assert_cc_matches!(actual_cc, quote! { :: }); | |
| } | |
| #[gtest] | |
| fn test_namespace_qualifier_basic() { | |
| let ns = NamespaceQualifier::new(["foo", "bar"], true); | |
| let actual_rs = ns.format_for_rs(); | |
| assert_rs_matches!(actual_rs, quote! { foo::bar:: }); | |
| let actual_cc = ns.format_for_cc(FlagSet::default()).unwrap(); | |
| assert_cc_matches!(actual_cc, quote! { :: foo::bar:: }); | |
| } | |
| #[gtest] | |
| fn test_namespace_qualifier_reserved_cc_keyword() { | |
| let ns = NamespaceQualifier::new(["foo", "impl", "bar"], true); | |
| let actual_rs = ns.format_for_rs(); | |
| assert_rs_matches!(actual_rs, quote! { foo :: r#impl :: bar :: }); | |
| let actual_cc = ns.format_for_cc(FlagSet::default()).unwrap(); | |
| assert_cc_matches!(actual_cc, quote! { :: foo::impl::bar:: }); | |
| } | |
| #[gtest] | |
| fn test_namespace_qualifier_reserved_rust_keyword() { | |
| let ns = NamespaceQualifier::new(["foo", "reinterpret_cast", "bar"], true); | |
| let actual_rs = ns.format_for_rs(); | |
| assert_rs_matches!(actual_rs, quote! { foo :: reinterpret_cast :: bar :: }); | |
| let cc_error = ns.format_for_cc(FlagSet::default()).unwrap_err(); | |
| let msg = cc_error.to_string(); | |
| assert!(msg.contains("`reinterpret_cast`")); | |
| assert!(msg.contains("C++ reserved word")); | |
| } | |
| #[gtest] | |
| fn test_format_cc_include_support_lib_header() { | |
| let tests = vec![ | |
| ( | |
| "\"crubit/support/path/for/test/{header}\"", | |
| "header.h", | |
| "\"crubit/support/path/for/test/header.h\"", | |
| ), | |
| ( | |
| "\"crubit/support/path/for/test/{header}\"", | |
| "subdir/header.h", | |
| "\"crubit/support/path/for/test/subdir/header.h\"", | |
| ), | |
| ( | |
| "<crubit/support/path/for/test/{header}>", | |
| "header.h", | |
| "<crubit/support/path/for/test/header.h>", | |
| ), | |
| ("\"{header}\"", "header.h", "\"header.h\""), | |
| ]; | |
| for (support_path_format, header, expected_output) in tests.iter() { | |
| let support_path_format = | |
| Format::parse_with_metavars(support_path_format, &["header"]).unwrap(); | |
| let header = | |
| CcInclude::support_lib_header(support_path_format, header.to_string().into()); | |
| let mut actual_tokens = TokenStream::default(); | |
| header.to_tokens(&mut actual_tokens); | |
| let expected_output: TokenStream = | |
| expected_output.parse().expect("Failed to convert expected_output to TokenStream"); | |
| assert_cc_matches!( | |
| actual_tokens, | |
| quote! { | |
| __HASH_TOKEN__ include #expected_output | |
| } | |
| ); | |
| } | |
| } | |
| #[gtest] | |
| fn test_escape_non_identifier_chars() { | |
| let tests = vec![ | |
| ("", ""), | |
| ("foo", "foo"), | |
| ("0abc", "_x00000030abc"), | |
| ("abc$xyz", "abc_dxyz"), | |
| ("abc.xyz", "abc_pxyz"), | |
| ("abc_xyz", "abc_uxyz"), | |
| ("abc🦀xyz", "abc_x0001f980xyz"), | |
| // With an escaping scheme like `$` => "_d", `<utf8 dd80 char>` => "_dd80", the | |
| // following 2 tests would fail the injectivity requirement (they both would map to | |
| // "_dd80"): | |
| ("$d80", "_dd80"), | |
| ("\u{740}", "_x00000740"), | |
| ]; | |
| for (input, expected_output) in tests.iter() { | |
| let actual_output = escape_non_identifier_chars(input); | |
| assert_eq!(&actual_output, expected_output); | |
| } | |
| // Asserting that each distinct, unique test input should result in a unique, | |
| // non-duplicated output. (This can be seen as a rather lightweight and | |
| // indirect verification of the injectivity requirement.) | |
| let duplicate_expectations = | |
| tests.iter().map(|(_, expected)| *expected).duplicates().collect_vec(); | |
| let empty_vec: Vec<&'static str> = vec![]; | |
| assert_eq!(empty_vec, duplicate_expectations); | |
| } | |
| #[gtest] | |
| fn test_is_cpp_pointer_type() { | |
| let tests = vec![ | |
| ("Foo", None), | |
| ("Foo*", Some(CcConstQualifier::Mut)), | |
| ("Foo const*", Some(CcConstQualifier::Const)), | |
| ("const Foo*", Some(CcConstQualifier::Const)), | |
| ("::foo::bar::Fizz * ", Some(CcConstQualifier::Mut)), | |
| ("::foo::bar::Fizz const *", Some(CcConstQualifier::Const)), | |
| ("const ::foo::bar::Fizz *", Some(CcConstQualifier::Const)), | |
| ]; | |
| for (input, expected_output) in tests.into_iter() { | |
| let actual_output = is_cpp_pointer_type(input.parse().unwrap()); | |
| assert_eq!(actual_output, expected_output); | |
| } | |
| } | |
| } |