GraphiteEditor/Graphite · error · syn::Error

command parameters must be plain identifiers

Error message

command parameters must be plain identifiers

What it means

Each parameter of a command function must be a plain identifier pattern (`name: Type`). The macro uses the identifier as the generated enum variant's field name, so tuple patterns, wildcard `_`, reference patterns (`&x`), or destructuring patterns cannot be represented and are rejected with a span on the whole `pat: Type` pair.

Source

Thrown at proc-macros/src/editor_commands.rs:68

		let signature = &function.sig;
		if let Some(receiver) = signature.receiver() {
			return Err(Error::new(receiver.span(), "command functions take no `self`; they are pure `args… -> Message` translations"));
		}
		if !signature.generics.params.is_empty() || signature.asyncness.is_some() || signature.unsafety.is_some() {
			return Err(Error::new(signature.span(), "command functions must be plain non-generic, non-async, safe functions"));
		}

		let docs = &function.attrs;
		let fn_name = &signature.ident;
		let variant = Ident::new(&fn_name.to_string().to_case(Case::Pascal), fn_name.span());
		let js_name = Ident::new(&fn_name.to_string().to_case(Case::Camel), fn_name.span());

		let mut param_names = Vec::new();
		let mut param_types = Vec::new();
		for parameter in &signature.inputs {
			let FnArg::Typed(pat_type) = parameter else { unreachable!("receiver is rejected above") };
			let Pat::Ident(pat_ident) = &*pat_type.pat else {
				return Err(Error::new(pat_type.span(), "command parameters must be plain identifiers"));
			};
			param_names.push(&pat_ident.ident);
			param_types.push(&*pat_type.ty);
		}

		let return_type = &signature.output;
		let body = &function.block;

		let span = fn_name.span();
		variants.extend(quote_spanned! {span=>
			#(#docs)*
			#variant { #(#param_names: #param_types,)* },
		});
		stubs.extend(quote_spanned! {span=>
			#(#docs)*
			#[cfg(not(feature = "native"))]
			#[wasm_bindgen(js_name = #js_name)]
			pub fn #fn_name(&self, #(#param_names: #param_types,)*) {

View on GitHub (pinned to c507b35645)

Solutions

  1. Split tuple destructuring into separate parameters: `fn move_layer(dx: f64, dy: f64)`.
  2. For `_`, keep the parameter named and prefix with underscore (`_unused: String`) — an underscore-prefixed ident is still `Pat::Ident`.
  3. For `&x`, take the value by reference at the type level (`x: &String`) instead of using a reference pattern.

Example fix

// before
#[editor_commands]
mod commands {
	fn move_layer((dx, dy): (f64, f64)) -> Message { ... }
	fn noop(_: String) -> Message { ... }
}

// after
#[editor_commands]
mod commands {
	fn move_layer(dx: f64, dy: f64) -> Message { ... }
	fn noop(_s: String) -> Message { ... }
}
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: Writing `fn move_layer((dx, dy): (f64, f64)) -> Message`, `fn foo(_: String) -> Message`, or `fn bar(&name: &String) -> Message` inside the #[editor_commands] module. Only `Pat::Ident` passes the let-else.

Common situations: Copy-pasting math-heavy code that destructures pairs inline, or silencing unused-parameter warnings with `_`. Common when commands wrap geometry tuples (Vec2 as a tuple) or Option destructuring.

Related errors


AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16). Data as JSON: /api/errors/211b4156c33a898b. Report an issue: GitHub.