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
- Delete the non-doc attribute from the command function.
- If you need #[cfg] gating, apply it to the whole #[editor_commands] module or split the gated command into a separately gated module.
- 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
- Allow only doc comments on command functions; use module-level #[cfg] for feature gating.
- Configure clippy/IDE snippets that insert attributes to skip the command module.
- Review diffs touching the commands module for stray attribute additions.
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
- Failed to parse node_fn attributes: {e}
- #[editor_commands] requires a module with an inline body
- only `use` imports and command functions may appear in an #[
- command functions have no visibility modifier; the macro gen
- command functions take no `self`; they are pure `args… -> Me
AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16).
Data as JSON: /api/errors/5eaf296fe511c854.
Report an issue: GitHub.