GraphiteEditor/Graphite · error · syn::Error

command functions have no visibility modifier; the macro gen

Error message

command functions have no visibility modifier; the macro generates the public JS-facing stub

What it means

Command functions declared inside an #[editor_commands] module must have inherited (private) visibility. The macro itself generates the public JS-facing dispatch stub, so a hand-written `pub`, `pub(crate)`, or `pub(in …)` modifier is a compile error — otherwise callers could bypass the generated stub or the visibility would conflict with it.

Source

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

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

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

View on GitHub (pinned to c507b35645)

Solutions

  1. Remove the visibility modifier so the command reads `fn name(args…) -> Message`.
  2. Keep the function private and rely on the macro-generated public stub for external access.
  3. If the function genuinely needs manual publicity outside the command dispatch, move it to a plain impl block instead of the #[editor_commands] module.

Example fix

// before
#[editor_commands]
mod commands {
	pub fn select_layer(layer: u64) -> Message { ... }
}

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

Strategy: validation

Prevention

When it happens

Trigger: Writing `pub fn`, `pub(crate) fn`, or any other visibility modifier on a function inside the #[editor_commands] module. The macro checks `matches!(function.vis, Visibility::Inherited)`.

Common situations: Copy-pasting an existing public command implementation from a plain module into the macro module, or instinctively marking new API surface as `pub` because it is called from JS elsewhere in the file.

Related errors


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