Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Naively change the case of method identifiers to comply with PSR-1 #63

Merged
merged 4 commits into from
Sep 19, 2021
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions ext-php-rs-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ proc-macro = true
[dependencies]
syn = { version = "1.0.68", features = ["full", "extra-traits"] }
darling = "0.12"
ident_case = "1.0.1"
quote = "1.0.9"
proc-macro2 = "1.0.26"
lazy_static = "1.4.0"
Expand Down
125 changes: 122 additions & 3 deletions ext-php-rs-derive/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use anyhow::{anyhow, bail, Result};
use darling::{FromMeta, ToTokens};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, ItemImpl, Lit, Meta, NestedMeta};
use syn::{Attribute, AttributeArgs, ItemImpl, Lit, Meta, NestedMeta};

use crate::{constant::Constant, method};

Expand All @@ -15,14 +15,77 @@ pub enum Visibility {
Private,
}

#[derive(Debug, Copy, Clone, FromMeta)]
pub enum RenameRule {
#[darling(rename = "none")]
None,
#[darling(rename = "camelCase")]
Camel,
#[darling(rename = "snake_case")]
Snake,
}

impl Default for RenameRule {
fn default() -> Self {
RenameRule::Camel
}
}

impl RenameRule {
/// Change case of an identifier.
///
/// Magic methods are handled specially to make sure they're always cased
/// correctly.
pub fn rename(&self, name: impl AsRef<str>) -> String {
let name = name.as_ref();
match self {
RenameRule::None => name.to_string(),
rule => match name {
"__construct" => "__construct".to_string(),
"__destruct" => "__destruct".to_string(),
"__call" => "__call".to_string(),
"__call_static" => "__callStatic".to_string(),
"__get" => "__get".to_string(),
"__set" => "__set".to_string(),
"__isset" => "__isset".to_string(),
"__unset" => "__unset".to_string(),
"__sleep" => "__sleep".to_string(),
"__wakeup" => "__wakeup".to_string(),
"__serialize" => "__serialize".to_string(),
"__unserialize" => "__unserialize".to_string(),
"__to_string" => "__toString".to_string(),
"__invoke" => "__invoke".to_string(),
"__set_state" => "__set_state".to_string(),
"__clone" => "__clone".to_string(),
"__debug_info" => "__debugInfo".to_string(),
field => match rule {
Self::Camel => ident_case::RenameRule::CamelCase.apply_to_field(field),
Self::Snake => ident_case::RenameRule::SnakeCase.apply_to_field(field),
Self::None => unreachable!(),
},
},
}
}
}

#[derive(Debug)]
pub enum ParsedAttribute {
Default(HashMap<String, Lit>),
Optional(String),
Visibility(Visibility),
Rename(String),
}

#[derive(Default, Debug, FromMeta)]
#[darling(default)]
pub struct AttrArgs {
rename_methods: Option<RenameRule>,
}

pub fn parser(input: ItemImpl) -> Result<TokenStream> {
pub fn parser(args: AttributeArgs, input: ItemImpl) -> Result<TokenStream> {
let args = AttrArgs::from_list(&args)
.map_err(|e| anyhow!("Unable to parse attribute arguments: {:?}", e))?;

let ItemImpl { self_ty, items, .. } = input;
let class_name = self_ty.to_token_stream().to_string();

Expand Down Expand Up @@ -61,7 +124,8 @@ pub fn parser(input: ItemImpl) -> Result<TokenStream> {
}
}
syn::ImplItem::Method(mut method) => {
let (sig, method) = method::parser(&mut method)?;
let (sig, method) =
method::parser(&mut method, args.rename_methods.unwrap_or_default())?;
class.methods.push(method);
sig
}
Expand Down Expand Up @@ -107,6 +171,61 @@ pub fn parse_attribute(attr: &Attribute) -> Result<ParsedAttribute> {
"public" => ParsedAttribute::Visibility(Visibility::Public),
"protected" => ParsedAttribute::Visibility(Visibility::Protected),
"private" => ParsedAttribute::Visibility(Visibility::Private),
"rename" => {
let ident = if let Meta::List(list) = meta {
if let Some(NestedMeta::Lit(lit)) = list.nested.first() {
String::from_value(lit).ok()
} else {
None
}
} else {
None
}
.ok_or_else(|| anyhow!("Invalid argument given for `#[rename] macro."))?;

ParsedAttribute::Rename(ident)
}
attr => bail!("Invalid attribute `#[{}]`.", attr),
})
}

#[cfg(test)]
mod tests {
use super::RenameRule;

#[test]
fn test_rename_magic() {
for &(magic, expected) in &[
("__construct", "__construct"),
("__destruct", "__destruct"),
("__call", "__call"),
("__call_static", "__callStatic"),
("__get", "__get"),
("__set", "__set"),
("__isset", "__isset"),
("__unset", "__unset"),
("__sleep", "__sleep"),
("__wakeup", "__wakeup"),
("__serialize", "__serialize"),
("__unserialize", "__unserialize"),
("__to_string", "__toString"),
("__invoke", "__invoke"),
("__set_state", "__set_state"),
("__clone", "__clone"),
("__debug_info", "__debugInfo"),
] {
assert_eq!(magic, RenameRule::None.rename(magic));
assert_eq!(expected, RenameRule::Camel.rename(magic));
assert_eq!(expected, RenameRule::Snake.rename(magic));
}
}

#[test]
fn test_rename_php_methods() {
for &(original, camel, snake) in &[("get_name", "getName", "get_name")] {
assert_eq!(original, RenameRule::None.rename(original));
assert_eq!(camel, RenameRule::Camel.rename(original));
assert_eq!(snake, RenameRule::Snake.rename(original));
}
}
}
5 changes: 3 additions & 2 deletions ext-php-rs-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,11 @@ pub fn php_startup(_: TokenStream, input: TokenStream) -> TokenStream {
}

#[proc_macro_attribute]
pub fn php_impl(_: TokenStream, input: TokenStream) -> TokenStream {
pub fn php_impl(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as AttributeArgs);
let input = parse_macro_input!(input as ItemImpl);

match impl_::parser(input) {
match impl_::parser(args, input) {
Ok(parsed) => parsed,
Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(),
}
Expand Down
12 changes: 9 additions & 3 deletions ext-php-rs-derive/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::collections::HashMap;

use crate::{
function,
impl_::{parse_attribute, ParsedAttribute, Visibility},
impl_::{parse_attribute, ParsedAttribute, RenameRule, Visibility},
};
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
Expand Down Expand Up @@ -34,16 +34,21 @@ pub struct Method {
pub visibility: Visibility,
}

pub fn parser(input: &mut ImplItemMethod) -> Result<(TokenStream, Method)> {
pub fn parser(
input: &mut ImplItemMethod,
rename_rule: RenameRule,
) -> Result<(TokenStream, Method)> {
let mut defaults = HashMap::new();
let mut optional = None;
let mut visibility = Visibility::Public;
let mut identifier = None;

for attr in input.attrs.iter() {
match parse_attribute(attr)? {
ParsedAttribute::Default(list) => defaults = list,
ParsedAttribute::Optional(name) => optional = Some(name),
ParsedAttribute::Visibility(vis) => visibility = vis,
ParsedAttribute::Rename(ident) => identifier = Some(ident),
}
}

Expand Down Expand Up @@ -92,8 +97,9 @@ pub fn parser(input: &mut ImplItemMethod) -> Result<(TokenStream, Method)> {
}
};

let name = identifier.unwrap_or_else(|| rename_rule.rename(ident.to_string()));
let method = Method {
name: ident.to_string(),
name,
ident: internal_ident.to_string(),
args,
optional,
Expand Down