tree-sitter/tree-sitter · error · Error

Grammar's 'word' property must be a named rule.

Error message

Grammar's 'word' property must be a named rule.

What it means

Thrown by `grammar({...})` when the `word` option's callback result has no string `name` property. `word` designates the rule used for keyword extraction, and it must be a reference to a named rule — the DSL takes `.name` of the returned value directly, without normalizing it, so only a symbol like `$.identifier` (whose object has `name`) satisfies the check. Inline tokens, patterns, strings, or composite nodes have no `name` and throw. (If you return a rule name that doesn't exist, the proxy stub's `name` is `'ReferenceError'`, which passes this check and is caught by the follow-up check 'must be a valid rule name'.)

Source

Thrown at crates/generate/src/dsl.js:379

    if (typeof options.extras !== "function") {
      throw new Error("Grammar's 'extras' property must be a function.");
    }

    extras = options.extras
      .call(ruleBuilder, ruleBuilder, baseGrammar.extras)

    if (!Array.isArray(extras)) {
      throw new Error("Grammar's 'extras' function must return an array.")
    }

    extras = extras.map(normalize);
  }

  let word = baseGrammar.word;
  if (options.word) {
    word = options.word.call(ruleBuilder, ruleBuilder).name;
    if (typeof word != 'string') {
      throw new Error("Grammar's 'word' property must be a named rule.");
    }

    if (word === 'ReferenceError') {
      throw new Error("Grammar's 'word' property must be a valid rule name.");
    }
  }

  let conflicts = baseGrammar.conflicts;
  if (options.conflicts) {
    if (typeof options.conflicts !== "function") {
      throw new Error("Grammar's 'conflicts' property must be a function.");
    }

    const baseConflictRules = baseGrammar.conflicts.map(conflict => conflict.map(sym));
    const conflictRules = options.conflicts.call(ruleBuilder, ruleBuilder, baseConflictRules);

    if (!Array.isArray(conflictRules)) {
      throw new Error("Grammar's conflicts must be an array of arrays of rules.");

View on GitHub (pinned to dff1fd868c)

Solutions

  1. Define the word token as a named rule, then reference the symbol: `identifier: $ => /[a-zA-Z_]\w*/` plus `word: $ => $.identifier`.
  2. Keep `word` pointing at exactly one named symbol — no `token()`, `seq()`, or literals in the return slot.
  3. If you got 'must be a valid rule name' instead, the referenced rule doesn't exist — check spelling/placement in `rules`.

Example fix

// before
word: $ => token(/[a-zA-Z_]\w*/)

// after
rules: {
  identifier: $ => /[a-zA-Z_]\w*/,
  ...
},
word: $ => $.identifier
Defensive patterns

Strategy: validation

Validate before calling

function validateWordOption(options) {
  if (options.word) {
    if (typeof options.word !== 'function') throw new Error('word must be a function');
    const $ = new Proxy({}, { get: (_, k) => ({ type: 'SYMBOL', name: k }) });
    const out = options.word($, undefined);
    if (typeof out?.name !== 'string' || out.name === 'ReferenceError') {
      throw new Error('word must return a named rule symbol, e.g. $ => $.identifier');
    }
  }
}

Type guard

const isNamedSymbol = (v) =>
  typeof v === 'object' && v !== null && typeof v.name === 'string' && v.name !== 'ReferenceError';

Prevention

When it happens

Trigger: `word: $ => token(/[a-zA-Z_]\w*/)` (inline token object has no `.name`); `word: $ => 'identifier'` (string primitive); `word: $ => seq(...)` or `choice(...)` (composite node); `word: $ => $.undefined_rule` (throws the follow-up 'valid rule name' error instead, at line 382).

Common situations: Authors inline the identifier token directly in the `word` slot instead of defining it as a named rule first; refactoring an inline identifier into `word` without moving the token into its own rule; grammars where the word rule is built by a helper returning a node rather than a symbol.

Related errors


AI-assisted analysis of tree-sitter/tree-sitter@dff1fd868c (2026-08-16). Data as JSON: /api/errors/59a5a985d7a7ab7b. Report an issue: GitHub.