GraphiteEditor/Graphite · error · syn::Error

command functions may not have attributes; anything that doe

Error message

command functions may not have attributes; anything that doesn't fit the `fn name(args…) -> Message` contract belongs in a plain impl block

What it means

Command functions inside an #[editor_commands] module may carry only doc comments; any other attribute is rejected. The macro re-emits each function as a generated JS-facing stub and synthesizes the enum variant itself, so extra attributes like #[cfg], #[inline], or derives have no defined place to go and would change semantics silently.

Source

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

	let mut imports: Vec<ItemUse> = Vec::new();
	let mut functions: Vec<ItemFn> = Vec::new();
	for item in items {
		match item {
			Item::Use(import) => imports.push(import),
			Item::Fn(function) => functions.push(function),
			other => return Err(Error::new(other.span(), "only `use` imports and command functions may appear in an #[editor_commands] module")),
		}
	}

	let mut variants = TokenStream::new();
	let mut stubs = TokenStream::new();
	let mut arms = TokenStream::new();

	for function in &functions {
		for attr in &function.attrs {
			if !attr.path().is_ident("doc") {
				return Err(Error::new(
					attr.span(),
					"command functions may not have attributes; anything that doesn't fit the `fn name(args…) -> Message` contract belongs in a plain impl block",
				));
			}
		}
		if !matches!(function.vis, Visibility::Inherited) {
			return Err(Error::new(
				function.span(),
				"command functions have no visibility modifier; the macro generates the public JS-facing stub",
			));
		}

		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"));

View on GitHub (pinned to c507b35645)

Solutions

  1. Delete the non-doc attribute from the command function.
  2. If you need #[cfg] gating, apply it to the whole #[editor_commands] module or split the gated command into a separately gated module.
  3. If you were annotating a helper rather than a command, move that helper into a plain impl block outside the macro module where attributes are unrestricted.

Example fix

// before
#[editor_commands]
mod commands {
	#[cfg(feature = "layers")]
	fn select_layer(layer: u64) -> Message { ... }
}

// after
#[cfg(feature = "layers")]
#[editor_commands]
mod commands {
	fn select_layer(layer: u64) -> Message { ... }
}
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: Adding #[cfg(feature = "…")], #[allow(…)], #[inline], #[test], or any derive/helper attribute to a function inside the #[editor_commands] module. Only `#[doc …]` (doc comments) passes the filter loop.

Common situations: Feature-gating a single command, silencing a clippy lint on one function, or annotating a command for documentation tooling. Developers used to sprinkling #[allow(clippy::…)] everywhere hit this quickly.

Related errors


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